use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum Language {
Rust,
TypeScript,
JavaScript,
Python,
Anchor,
Go,
}
impl std::fmt::Display for Language {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Rust => write!(f, "rust"),
Self::TypeScript => write!(f, "typescript"),
Self::JavaScript => write!(f, "javascript"),
Self::Python => write!(f, "python"),
Self::Anchor => write!(f, "anchor"),
Self::Go => write!(f, "go"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Project {
pub name: String,
pub path: PathBuf,
pub language: Language,
pub manifest_path: PathBuf,
pub context_summary: Option<String>,
pub public_api_files: Vec<PathBuf>,
pub internal_files: Vec<PathBuf>,
pub content_hash: [u8; 32],
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum DepKind {
Source,
Build,
Dev,
Cpi,
}
impl std::fmt::Display for DepKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Source => write!(f, "source"),
Self::Build => write!(f, "build"),
Self::Dev => write!(f, "dev"),
Self::Cpi => write!(f, "cpi"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dependency {
pub name: String,
pub version_req: String,
pub kind: DepKind,
pub is_workspace: bool,
pub resolved_path: Option<PathBuf>,
}
#[derive(Debug, thiserror::Error, miette::Diagnostic)]
pub enum KdoError {
#[error("workspace manifest not found at {0}")]
ManifestNotFound(PathBuf),
#[error("failed to parse {path}: {source}")]
ParseError {
path: PathBuf,
source: anyhow::Error,
},
#[error("project not found: {0}")]
ProjectNotFound(String),
#[error("circular dependency detected: {0}")]
#[diagnostic(help("break the cycle by extracting shared code into a separate crate"))]
CircularDependency(String),
#[error(transparent)]
Io(#[from] std::io::Error),
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct WorkspaceConfig {
pub workspace: WorkspaceMeta,
#[serde(default)]
pub tasks: std::collections::BTreeMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct WorkspaceMeta {
#[serde(default)]
pub name: String,
}
impl WorkspaceConfig {
pub fn load(path: &std::path::Path) -> Result<Self, KdoError> {
let content = std::fs::read_to_string(path)?;
toml::from_str(&content).map_err(|e| KdoError::ParseError {
path: path.to_path_buf(),
source: e.into(),
})
}
pub fn save(&self, path: &std::path::Path) -> Result<(), KdoError> {
let content = toml::to_string_pretty(self).map_err(|e| KdoError::ParseError {
path: path.to_path_buf(),
source: e.into(),
})?;
std::fs::write(path, content)?;
Ok(())
}
}
pub fn estimate_tokens(s: &str) -> usize {
s.len() / 4
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_estimate_tokens() {
assert_eq!(estimate_tokens(""), 0);
assert_eq!(estimate_tokens("abcd"), 1);
assert_eq!(estimate_tokens("ab"), 0);
assert_eq!(estimate_tokens("hello world!"), 3);
}
#[test]
fn test_language_display() {
assert_eq!(Language::Rust.to_string(), "rust");
assert_eq!(Language::Anchor.to_string(), "anchor");
}
#[test]
fn test_language_serde_roundtrip() {
let lang = Language::TypeScript;
let json = serde_json::to_string(&lang).unwrap();
assert_eq!(json, "\"typescript\"");
let back: Language = serde_json::from_str(&json).unwrap();
assert_eq!(back, lang);
}
#[test]
fn test_dep_kind_display() {
assert_eq!(DepKind::Cpi.to_string(), "cpi");
assert_eq!(DepKind::Source.to_string(), "source");
}
}