1use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
11#[serde(rename_all = "lowercase")]
12pub enum Language {
13 Rust,
15 TypeScript,
17 JavaScript,
19 Python,
21 Anchor,
23 Go,
25}
26
27impl std::fmt::Display for Language {
28 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29 match self {
30 Self::Rust => write!(f, "rust"),
31 Self::TypeScript => write!(f, "typescript"),
32 Self::JavaScript => write!(f, "javascript"),
33 Self::Python => write!(f, "python"),
34 Self::Anchor => write!(f, "anchor"),
35 Self::Go => write!(f, "go"),
36 }
37 }
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct Project {
43 pub name: String,
45 pub path: PathBuf,
47 pub language: Language,
49 pub manifest_path: PathBuf,
51 pub context_summary: Option<String>,
53 pub public_api_files: Vec<PathBuf>,
55 pub internal_files: Vec<PathBuf>,
57 pub content_hash: [u8; 32],
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
63#[serde(rename_all = "lowercase")]
64pub enum DepKind {
65 Source,
67 Build,
69 Dev,
71 Cpi,
73}
74
75impl std::fmt::Display for DepKind {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 match self {
78 Self::Source => write!(f, "source"),
79 Self::Build => write!(f, "build"),
80 Self::Dev => write!(f, "dev"),
81 Self::Cpi => write!(f, "cpi"),
82 }
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct Dependency {
89 pub name: String,
91 pub version_req: String,
93 pub kind: DepKind,
95 pub is_workspace: bool,
97 pub resolved_path: Option<PathBuf>,
99}
100
101#[derive(Debug, thiserror::Error, miette::Diagnostic)]
103pub enum KdoError {
104 #[error("workspace manifest not found at {0}")]
106 ManifestNotFound(PathBuf),
107
108 #[error("failed to parse {path}: {source}")]
110 ParseError {
111 path: PathBuf,
113 source: anyhow::Error,
115 },
116
117 #[error("project not found: {0}")]
119 ProjectNotFound(String),
120
121 #[error("circular dependency detected: {0}")]
123 #[diagnostic(help("break the cycle by extracting shared code into a separate crate"))]
124 CircularDependency(String),
125
126 #[error(transparent)]
128 Io(#[from] std::io::Error),
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize, Default)]
133pub struct WorkspaceConfig {
134 pub workspace: WorkspaceMeta,
136 #[serde(default)]
138 pub tasks: std::collections::BTreeMap<String, String>,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize, Default)]
143pub struct WorkspaceMeta {
144 #[serde(default)]
146 pub name: String,
147}
148
149impl WorkspaceConfig {
150 pub fn load(path: &std::path::Path) -> Result<Self, KdoError> {
152 let content = std::fs::read_to_string(path)?;
153 toml::from_str(&content).map_err(|e| KdoError::ParseError {
154 path: path.to_path_buf(),
155 source: e.into(),
156 })
157 }
158
159 pub fn save(&self, path: &std::path::Path) -> Result<(), KdoError> {
161 let content = toml::to_string_pretty(self).map_err(|e| KdoError::ParseError {
162 path: path.to_path_buf(),
163 source: e.into(),
164 })?;
165 std::fs::write(path, content)?;
166 Ok(())
167 }
168}
169
170pub fn estimate_tokens(s: &str) -> usize {
179 s.len() / 4
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn test_estimate_tokens() {
188 assert_eq!(estimate_tokens(""), 0);
189 assert_eq!(estimate_tokens("abcd"), 1);
190 assert_eq!(estimate_tokens("ab"), 0);
191 assert_eq!(estimate_tokens("hello world!"), 3);
192 }
193
194 #[test]
195 fn test_language_display() {
196 assert_eq!(Language::Rust.to_string(), "rust");
197 assert_eq!(Language::Anchor.to_string(), "anchor");
198 }
199
200 #[test]
201 fn test_language_serde_roundtrip() {
202 let lang = Language::TypeScript;
203 let json = serde_json::to_string(&lang).unwrap();
204 assert_eq!(json, "\"typescript\"");
205 let back: Language = serde_json::from_str(&json).unwrap();
206 assert_eq!(back, lang);
207 }
208
209 #[test]
210 fn test_dep_kind_display() {
211 assert_eq!(DepKind::Cpi.to_string(), "cpi");
212 assert_eq!(DepKind::Source.to_string(), "source");
213 }
214}