nd / nd-core/src/models.rs

raw file 3509 bytes
use serde::{Deserialize, Serialize};

/// A project as served by the hub / consumed by the cli.
/// `files` is only filled in for single-project lookups (not lists).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Project {
    pub slug: String,
    pub name: String,
    /// "app" (default) or "template" — templates are skeletons pulled by agents
    #[serde(default)]
    pub kind: String,
    pub version: String,
    pub description: String,
    pub llm_description: String,
    pub keywords: Vec<String>,
    pub code_keywords: Vec<String>,
    pub languages: Vec<String>,
    pub author: String,
    pub license: String,
    pub homepage: String,
    pub entry_points: Vec<String>,
    pub install: String,
    pub run: String,
    pub model: String,
    pub prompt_summary: String,
    pub file_count: i64,
    pub total_bytes: i64,
    pub downloads: i64,
    pub views: i64,
    pub created_at: String,
    pub updated_at: String,
    /// machine specs as free-form json (see nd_core::specs::Specs)
    #[serde(default)]
    pub specs: Option<serde_json::Value>,
    /// maintainer public key (base64 ed25519), set by the first signed upload
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub owner: Option<String>,
    /// slug of the project this one forked from, if any
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub fork_of: Option<String>,
    /// short plain-text readme excerpt, filled in for list views / cards
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub readme_excerpt: Option<String>,
    #[serde(default)]
    pub files: Vec<String>,
}

impl Project {
    /// "2026-10-16T12:00:00+00:00" -> "2026-10-16"
    pub fn short_updated(&self) -> String {
        self.updated_at.split('T').next().unwrap_or("").to_string()
    }

    /// First 8 chars of the maintainer key, if owned.
    pub fn owner_short(&self) -> Option<String> {
        self.owner.as_deref().map(|o| o.chars().take(8).collect())
    }

    /// Canonical url path under /p/: "<owner8>/<slug>" when owned, else "<slug>".
    pub fn url_path(&self) -> String {
        match self.owner_short() {
            Some(o) => format!("{o}/{}", self.slug),
            None => self.slug.clone(),
        }
    }

    /// The ref `nd download <ref>` accepts — same shape as url_path.
    pub fn download_ref(&self) -> String {
        self.url_path()
    }
}

/// A review/feedback entry on a project — what happened when someone
/// (often an llm agent) downloaded and tried it.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Review {
    pub id: i64,
    pub slug: String,
    pub author: String,
    /// suggested: "works" | "issues" | "broken" | "notes" (free text allowed)
    pub verdict: String,
    pub body: String,
    pub created_at: String,
}

impl Review {
    /// "2026-10-16T12:00:00+00:00" -> "2026-10-16"
    pub fn short_date(&self) -> String {
        self.created_at.split('T').next().unwrap_or("").to_string()
    }
}

/// A maintainer-posted update (changelog entry) on a project.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ProjectUpdate {
    pub id: i64,
    pub slug: String,
    pub author: String,
    pub title: String,
    pub body: String,
    pub created_at: String,
}

impl ProjectUpdate {
    /// "2026-10-16T12:00:00+00:00" -> "2026-10-16"
    pub fn short_date(&self) -> String {
        self.created_at.split('T').next().unwrap_or("").to_string()
    }
}