arxivis 1.0.0

Official Rust SDK for the Arxivis document store
Documentation
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

// ── Core record types ─────────────────────────────────────────────────────────

/// Canonical metadata for a stored file.
#[derive(Debug, Clone, Deserialize)]
pub struct FileRecord {
    /// UUID that uniquely identifies the file.
    pub id: String,
    /// The filename as originally uploaded.
    pub original_name: String,
    /// Uncompressed file size in bytes.
    pub size: i64,
    /// On-disk size after optional compression.
    pub compressed_size: i64,
    /// Hex-encoded SHA-256 digest of the original content.
    pub sha256: String,
    /// MIME type detected or provided at upload time.
    pub mime_type: String,
    /// `true` when the file is stored with zstd compression.
    pub is_compressed: bool,
    /// `true` when the file is stored with AES-GCM encryption.
    pub is_encrypted: bool,
    /// User-supplied tags.
    pub tags: Vec<String>,
    /// Virtual folder path, e.g. `"/invoices/2024/"`.
    pub parent_path: String,
    /// UTC timestamp of initial upload.
    pub created_at: DateTime<Utc>,
    /// UTC timestamp of last metadata update.
    pub updated_at: DateTime<Utc>,
}

/// Paginated file listing returned by [`ArxivisClient::list`] and
/// [`ArxivisClient::search`].
#[derive(Debug, Clone, Deserialize)]
pub struct ListResult {
    /// Files in this page.
    pub files: Vec<FileRecord>,
    /// Total number of matching files (across all pages).
    pub total: i64,
    /// Page size used for this query.
    pub limit: i64,
    /// Zero-based offset used for this query.
    pub offset: i64,
}

/// Result returned by the semantic and hybrid search endpoints.
#[derive(Debug, Clone, Deserialize)]
pub struct SearchResult {
    /// Ranked list of matching files.
    pub files: Vec<FileRecord>,
    /// Number of files returned.
    pub total: i64,
    /// Search mode used by the server (`"semantic"` or `"hybrid"`).
    pub mode: String,
}

/// Aggregate storage statistics.
#[derive(Debug, Clone, Deserialize)]
pub struct Stats {
    /// Total number of non-deleted files.
    pub total_files: i64,
    /// Total uncompressed size of all files in bytes.
    pub total_size: i64,
    /// Total on-disk size after compression in bytes.
    pub compressed_size: i64,
    /// Bytes saved by compression (`total_size - compressed_size`).
    pub saved_bytes: i64,
}

/// An API key record (the secret is never included after creation).
#[derive(Debug, Clone, Deserialize)]
pub struct ApiKey {
    /// UUID of the key.
    pub id: String,
    /// Human-readable display name.
    pub name: String,
    /// Short prefix used to identify the key without revealing the secret.
    pub key_prefix: String,
    /// UTC creation timestamp.
    pub created_at: DateTime<Utc>,
    /// UTC timestamp of the last time this key was used, if ever.
    pub last_used_at: Option<DateTime<Utc>>,
    /// `false` once the key has been revoked.
    pub is_active: bool,
}

/// Returned by [`ArxivisClient::create_key`].
/// **Store the `key` field immediately — it is shown only once.**
#[derive(Debug, Clone, Deserialize)]
pub struct CreateKeyResult {
    /// UUID of the new key.
    pub id: String,
    /// Human-readable display name.
    pub name: String,
    /// Short prefix of the key.
    pub key_prefix: String,
    /// UTC creation timestamp.
    pub created_at: DateTime<Utc>,
    /// The full API key string (`axv_<prefix>_<secret>`).
    /// This value is returned **only once** and cannot be recovered.
    pub key: String,
}

// ── Option / builder types ────────────────────────────────────────────────────

/// Options for [`ArxivisClient::upload`] / [`ArxivisClient::upload_bytes`].
///
/// Use the builder methods to set only the fields you need:
///
/// ```rust
/// use arxivis::UploadOptions;
///
/// let opts = UploadOptions::new()
///     .path("/invoices/2024/")
///     .tags(vec!["cliente".into(), "enero".into()])
///     .compress(true);
/// ```
#[derive(Debug, Default, Clone)]
pub struct UploadOptions {
    /// User-supplied tags attached to the file.
    pub tags: Vec<String>,
    /// Virtual folder path, e.g. `"/invoices/2024/"`.
    /// Defaults to `"/"` when `None`.
    pub path: Option<String>,
    /// Override the server-default compression policy.
    pub compress: Option<bool>,
    /// Override the server-default encryption policy.
    pub encrypt: Option<bool>,
}

impl UploadOptions {
    /// Create a new `UploadOptions` with all fields at their defaults.
    pub fn new() -> Self {
        Self::default()
    }

    /// Set the tags for this file.
    pub fn tags(mut self, tags: Vec<String>) -> Self {
        self.tags = tags;
        self
    }

    /// Set the virtual folder path (e.g. `"/invoices/2024/"`).
    pub fn path(mut self, path: impl Into<String>) -> Self {
        self.path = Some(path.into());
        self
    }

    /// Enable or disable zstd compression for this file.
    pub fn compress(mut self, c: bool) -> Self {
        self.compress = Some(c);
        self
    }

    /// Enable or disable AES-GCM encryption for this file.
    pub fn encrypt(mut self, e: bool) -> Self {
        self.encrypt = Some(e);
        self
    }
}

/// Pagination options for [`ArxivisClient::list`].
#[derive(Debug, Default, Clone)]
pub struct ListOptions {
    /// Maximum number of results to return. The server caps this at 200.
    /// Defaults to 50 when `0` or not set.
    pub limit: usize,
    /// Zero-based offset into the result set for pagination.
    pub offset: usize,
}

impl ListOptions {
    /// Create `ListOptions` with all defaults.
    pub fn new() -> Self {
        Self::default()
    }

    /// Set the page size.
    pub fn limit(mut self, limit: usize) -> Self {
        self.limit = limit;
        self
    }

    /// Set the page offset.
    pub fn offset(mut self, offset: usize) -> Self {
        self.offset = offset;
        self
    }
}

/// Options for search endpoints.
#[derive(Debug, Default, Clone)]
pub struct SearchOptions {
    /// Maximum number of results to return.
    /// Defaults to 50 for full-text search and 20 for semantic / hybrid.
    pub limit: usize,
    /// Zero-based offset (supported by full-text search; ignored by semantic).
    pub offset: usize,
}

impl SearchOptions {
    /// Create `SearchOptions` with all defaults.
    pub fn new() -> Self {
        Self::default()
    }

    /// Set the result limit.
    pub fn limit(mut self, limit: usize) -> Self {
        self.limit = limit;
        self
    }

    /// Set the page offset.
    pub fn offset(mut self, offset: usize) -> Self {
        self.offset = offset;
        self
    }
}

// ── Internal wire types ───────────────────────────────────────────────────────

/// Wire shape for the `POST /keys` request body.
#[derive(Serialize)]
pub(crate) struct CreateKeyRequest<'a> {
    pub name: &'a str,
}

/// Wire shape for the `POST /files/zip` request body.
#[derive(Serialize)]
pub(crate) struct ZipRequest<'a> {
    pub ids: &'a [&'a str],
}

/// Wire shape for the `GET /keys` response envelope.
#[derive(Deserialize)]
pub(crate) struct KeysEnvelope {
    pub keys: Vec<ApiKey>,
}