1use crate::{CodegenError, Result};
8use cuengine::ModuleEvalOptions;
9use cuenv_core::ModuleEvaluation;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::path::{Path, PathBuf};
13
14#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "lowercase")]
17pub enum FileMode {
18 #[default]
20 Managed,
21 Scaffold,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct FormatConfig {
28 pub indent: String,
30 #[serde(rename = "indentSize")]
32 pub indent_size: Option<usize>,
33 #[serde(rename = "lineWidth")]
35 pub line_width: Option<usize>,
36 #[serde(rename = "trailingComma")]
38 pub trailing_comma: Option<String>,
39 pub semicolons: Option<bool>,
41 pub quotes: Option<String>,
43}
44
45impl Default for FormatConfig {
46 fn default() -> Self {
47 Self {
48 indent: "space".to_string(),
49 indent_size: Some(2),
50 line_width: Some(100),
51 trailing_comma: None,
52 semicolons: None,
53 quotes: None,
54 }
55 }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ProjectFileDefinition {
61 pub content: String,
63 pub language: String,
65 #[serde(default)]
67 pub mode: FileMode,
68 #[serde(default)]
70 pub format: FormatConfig,
71 #[serde(default)]
73 pub gitignore: bool,
74}
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct CubeData {
79 pub files: HashMap<String, ProjectFileDefinition>,
81 #[serde(default)]
83 pub context: serde_json::Value,
84}
85
86#[derive(Debug)]
92pub struct Cube {
93 pub data: CubeData,
95 pub source_path: PathBuf,
97}
98
99impl Cube {
100 pub fn load(path: impl AsRef<Path>) -> Result<Self> {
106 let path = path.as_ref();
107
108 let data = Self::evaluate_cue(path)?;
111
112 Ok(Self {
113 data,
114 source_path: path.to_path_buf(),
115 })
116 }
117
118 #[must_use]
120 pub fn files(&self) -> &HashMap<String, ProjectFileDefinition> {
121 &self.data.files
122 }
123
124 #[must_use]
126 pub fn context(&self) -> &serde_json::Value {
127 &self.data.context
128 }
129
130 #[must_use]
132 pub fn source_path(&self) -> &Path {
133 &self.source_path
134 }
135
136 fn evaluate_cue(path: &Path) -> Result<CubeData> {
138 if !path.exists() {
140 return Err(CodegenError::Cube(format!(
141 "Cube file not found: {}",
142 path.display()
143 )));
144 }
145
146 let dir_path = path.parent().ok_or_else(|| {
148 CodegenError::Cube("Invalid cube path: no parent directory".to_string())
149 })?;
150
151 let package_name = Self::determine_package_name(path)?;
153
154 let module_root = Self::find_cue_module_root(dir_path).ok_or_else(|| {
156 CodegenError::Cube(format!(
157 "No CUE module found (looking for cue.mod/) starting from: {}",
158 dir_path.display()
159 ))
160 })?;
161
162 let options = ModuleEvalOptions {
164 recursive: true,
165 ..Default::default()
166 };
167 let raw_result = cuengine::evaluate_module(&module_root, &package_name, Some(options))
168 .map_err(|e| CodegenError::Cube(format!("CUE evaluation failed: {e}")))?;
169
170 let module = ModuleEvaluation::from_raw(
171 module_root.clone(),
172 raw_result.instances,
173 raw_result.projects,
174 );
175
176 let target_path = dir_path
178 .canonicalize()
179 .map_err(|e| CodegenError::Cube(format!("Failed to canonicalize path: {e}")))?;
180 let relative_path = target_path.strip_prefix(&module_root).map_or_else(
181 |_| PathBuf::from("."),
182 |p| {
183 if p.as_os_str().is_empty() {
184 PathBuf::from(".")
185 } else {
186 p.to_path_buf()
187 }
188 },
189 );
190
191 let instance = module.get(&relative_path).ok_or_else(|| {
192 CodegenError::Cube(format!(
193 "No CUE instance found at path: {} (relative: {})",
194 dir_path.display(),
195 relative_path.display()
196 ))
197 })?;
198
199 instance
200 .deserialize()
201 .map_err(|e| CodegenError::Cube(format!("Failed to deserialize cube data: {e}")))
202 }
203
204 fn find_cue_module_root(start: &Path) -> Option<PathBuf> {
206 let mut current = start.canonicalize().ok()?;
207 loop {
208 if current.join("cue.mod").is_dir() {
209 return Some(current);
210 }
211 if !current.pop() {
212 return None;
213 }
214 }
215 }
216
217 fn determine_package_name(path: &Path) -> Result<String> {
222 let content = std::fs::read_to_string(path)
223 .map_err(|e| CodegenError::Cube(format!("Failed to read cube file: {e}")))?;
224
225 for line in content.lines().take(10) {
227 let trimmed = line.trim();
228 if trimmed.starts_with("package ") {
229 let package_name = trimmed.strip_prefix("package ").unwrap_or("cubes").trim();
231 return Ok(package_name.to_string());
232 }
233 }
234
235 Ok("cubes".to_string())
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243
244 #[test]
245 fn test_file_mode_default() {
246 assert_eq!(FileMode::default(), FileMode::Managed);
247 }
248
249 #[test]
250 fn test_format_config_default() {
251 let config = FormatConfig::default();
252 assert_eq!(config.indent, "space");
253 assert_eq!(config.indent_size, Some(2));
254 assert_eq!(config.line_width, Some(100));
255 }
256}