#[cfg(not(target_arch = "wasm32"))]
use std::path::{Path, PathBuf};
use anyhow::Result;
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct ProjectState {
pub project: Project,
pub current_file: Option<String>,
}
impl ProjectState {
#[cfg(not(target_arch = "wasm32"))]
pub async fn new_from_path(path: PathBuf) -> Result<ProjectState> {
let source_path = if path == Path::new(".") {
std::env::current_dir().map_err(|e| anyhow::anyhow!("Error getting the current directory: {:?}", e))?
} else {
path
};
let source_path =
std::path::Path::new(&urlencoding::decode(&source_path.display().to_string())?.to_string()).to_path_buf();
let source_path = if source_path.is_relative() {
std::env::current_dir()
.map_err(|e| anyhow::anyhow!("Error getting the current directory: {:?}", e))?
.join(source_path)
} else {
source_path
};
if source_path.is_dir() {
let project = Project::from_path(&source_path)
.await
.map_err(|e| anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e))?;
let project_file = source_path.join(crate::settings::types::DEFAULT_PROJECT_KCL_FILE);
if !project_file.exists() {
tokio::fs::write(&project_file, vec![]).await?;
}
return Ok(ProjectState {
project,
current_file: Some(project_file.display().to_string()),
});
}
let extension = source_path
.extension()
.ok_or_else(|| anyhow::anyhow!("Error getting the extension of the file: `{}`", source_path.display()))?;
let ext = extension.to_string_lossy().to_string();
if !crate::settings::utils::RELEVANT_EXTENSIONS.contains(&ext) || ext == "toml" {
return Err(anyhow::anyhow!(
"File type ({}) cannot be opened with this app: `{}`, try opening one of the following file types: {}",
ext,
source_path.display(),
crate::settings::utils::RELEVANT_EXTENSIONS.join(", ")
));
}
let parent = source_path.parent().ok_or_else(|| {
anyhow::anyhow!(
"Error getting the parent directory of the file: {}",
source_path.display()
)
})?;
if crate::settings::utils::IMPORT_FILE_EXTENSIONS.contains(&ext) {
let import_file_name = source_path
.file_name()
.ok_or_else(|| anyhow::anyhow!("Error getting the file name of the file: {}", source_path.display()))?
.to_string_lossy()
.to_string();
let kcl_wrapper_filename = format!("{}.kcl", import_file_name);
let kcl_wrapper_file_path = parent.join(&kcl_wrapper_filename);
if !kcl_wrapper_file_path.exists() {
tokio::fs::write(
&kcl_wrapper_file_path,
format!(
r#"// This file was automatically generated by the application when you
// double-clicked on the model file.
// You can edit this file to add your own content.
// But we recommend you keep the import statement as it is.
// For more information on the import statement, see the documentation at:
// https://zoo.dev/docs/kcl/import
const model = import("{}")"#,
import_file_name
)
.as_bytes(),
)
.await?;
}
let project = Project::from_path(&parent)
.await
.map_err(|e| anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e))?;
return Ok(ProjectState {
project,
current_file: Some(kcl_wrapper_file_path.display().to_string()),
});
}
let project = Project::from_path(&parent)
.await
.map_err(|e| anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e))?;
Ok(ProjectState {
project,
current_file: Some(source_path.display().to_string()),
})
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct Project {
#[serde(flatten)]
pub file: FileEntry,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<FileMetadata>,
#[serde(default)]
#[ts(type = "number")]
pub kcl_file_count: u64,
#[serde(default)]
#[ts(type = "number")]
pub directory_count: u64,
pub default_file: String,
}
impl Project {
#[cfg(not(target_arch = "wasm32"))]
pub async fn from_path<P: AsRef<std::path::Path>>(path: P) -> Result<Self> {
let path = if path.as_ref() == std::path::Path::new(".") {
std::env::current_dir()?
} else {
path.as_ref().to_path_buf()
};
if !path.exists() {
return Err(anyhow::anyhow!("Path does not exist"));
}
let file = crate::settings::utils::walk_dir(&path).await?;
let metadata = std::fs::metadata(&path).ok().map(|m| m.into());
let mut project = Self {
file: file.clone(),
metadata,
kcl_file_count: 0,
directory_count: 0,
default_file: get_default_kcl_file_for_dir(path, file).await?,
};
project.populate_kcl_file_count()?;
project.populate_directory_count()?;
Ok(project)
}
pub fn populate_kcl_file_count(&mut self) -> Result<()> {
let mut count = 0;
if let Some(children) = &self.file.children {
for entry in children.iter() {
if entry.name.ends_with(".kcl") {
count += 1;
} else {
count += entry.kcl_file_count();
}
}
}
self.kcl_file_count = count;
Ok(())
}
pub fn populate_directory_count(&mut self) -> Result<()> {
let mut count = 0;
if let Some(children) = &self.file.children {
for entry in children.iter() {
count += entry.directory_count();
}
}
self.directory_count = count;
Ok(())
}
}
#[cfg(not(target_arch = "wasm32"))]
#[async_recursion::async_recursion]
pub async fn get_default_kcl_file_for_dir<P>(dir: P, file: FileEntry) -> Result<String>
where
P: AsRef<Path> + Send,
{
if !dir.as_ref().is_dir() {
return Err(anyhow::anyhow!("Path `{}` is not a directory", dir.as_ref().display()));
}
let default_file = dir.as_ref().join(crate::settings::types::DEFAULT_PROJECT_KCL_FILE);
if !default_file.exists() {
if let Some(children) = file.children {
for entry in children.iter() {
if entry.name.ends_with(".kcl") {
return Ok(dir.as_ref().join(&entry.name).display().to_string());
} else if entry.children.is_some() {
return get_default_kcl_file_for_dir(entry.path.clone(), entry.clone()).await;
}
}
}
tokio::fs::write(&default_file, vec![]).await?;
}
Ok(default_file.display().to_string())
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn rename_project_directory<P>(path: P, new_name: &str) -> Result<std::path::PathBuf>
where
P: AsRef<Path> + Send,
{
if new_name.is_empty() {
return Err(anyhow::anyhow!("New name for project cannot be empty"));
}
if !path.as_ref().is_dir() {
return Err(anyhow::anyhow!("Path `{}` is not a directory", path.as_ref().display()));
}
let new_path = path
.as_ref()
.parent()
.ok_or_else(|| anyhow::anyhow!("Parent directory of `{}` not found", path.as_ref().display()))?
.join(new_name);
if new_path.exists() {
return Err(anyhow::anyhow!(
"Path `{}` already exists, cannot rename to an existing path",
new_path.display()
));
}
tokio::fs::rename(path.as_ref(), &new_path).await?;
Ok(new_path)
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct FileEntry {
pub path: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub children: Option<Vec<FileEntry>>,
}
impl FileEntry {
pub fn kcl_file_count(&self) -> u64 {
let mut count = 0;
if let Some(children) = &self.children {
for entry in children.iter() {
if entry.name.ends_with(".kcl") {
count += 1;
} else {
count += entry.kcl_file_count();
}
}
}
count
}
pub fn directory_count(&self) -> u64 {
let mut count = 0;
if let Some(children) = &self.children {
for entry in children.iter() {
if entry.children.is_some() {
count += 1;
}
}
}
count
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
pub struct FileMetadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub accessed: Option<chrono::DateTime<chrono::Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created: Option<chrono::DateTime<chrono::Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub r#type: Option<FileType>,
#[serde(default)]
#[ts(type = "number")]
pub size: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub modified: Option<chrono::DateTime<chrono::Utc>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub permission: Option<FilePermission>,
}
#[derive(Debug, Copy, Clone, Deserialize, Serialize, JsonSchema, Display, FromStr, ts_rs::TS, PartialEq, Eq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
#[display(style = "snake_case")]
pub enum FileType {
File,
Directory,
Symlink,
}
#[derive(Debug, Copy, Clone, Deserialize, Serialize, JsonSchema, Display, FromStr, ts_rs::TS, PartialEq, Eq)]
#[ts(export)]
#[serde(rename_all = "snake_case")]
#[display(style = "snake_case")]
pub enum FilePermission {
Read,
Write,
Execute,
}
impl From<std::fs::FileType> for FileType {
fn from(file_type: std::fs::FileType) -> Self {
if file_type.is_file() {
FileType::File
} else if file_type.is_dir() {
FileType::Directory
} else if file_type.is_symlink() {
FileType::Symlink
} else {
unreachable!()
}
}
}
impl From<std::fs::Permissions> for FilePermission {
fn from(permissions: std::fs::Permissions) -> Self {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = permissions.mode();
if mode & 0o400 != 0 {
FilePermission::Read
} else if mode & 0o200 != 0 {
FilePermission::Write
} else if mode & 0o100 != 0 {
FilePermission::Execute
} else {
unreachable!()
}
}
#[cfg(not(unix))]
{
if permissions.readonly() {
FilePermission::Read
} else {
FilePermission::Write
}
}
}
}
impl From<std::fs::Metadata> for FileMetadata {
fn from(metadata: std::fs::Metadata) -> Self {
Self {
accessed: metadata.accessed().ok().map(|t| t.into()),
created: metadata.created().ok().map(|t| t.into()),
r#type: Some(metadata.file_type().into()),
size: metadata.len(),
modified: metadata.modified().ok().map(|t| t.into()),
permission: Some(metadata.permissions().into()),
}
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
#[tokio::test]
async fn test_default_kcl_file_for_dir_non_exist() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&dir).unwrap();
let file = crate::settings::utils::walk_dir(&dir).await.unwrap();
let default_file = super::get_default_kcl_file_for_dir(&dir, file).await.unwrap();
assert_eq!(default_file, dir.join("main.kcl").display().to_string());
std::fs::remove_dir_all(dir).unwrap();
}
#[tokio::test]
async fn test_default_kcl_file_for_dir_main_kcl() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("main.kcl"), vec![]).unwrap();
let file = crate::settings::utils::walk_dir(&dir).await.unwrap();
let default_file = super::get_default_kcl_file_for_dir(&dir, file).await.unwrap();
assert_eq!(default_file, dir.join("main.kcl").display().to_string());
std::fs::remove_dir_all(dir).unwrap();
}
#[tokio::test]
async fn test_default_kcl_file_for_dir_thing_kcl() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("thing.kcl"), vec![]).unwrap();
let file = crate::settings::utils::walk_dir(&dir).await.unwrap();
let default_file = super::get_default_kcl_file_for_dir(&dir, file).await.unwrap();
assert_eq!(default_file, dir.join("thing.kcl").display().to_string());
std::fs::remove_dir_all(dir).unwrap();
}
#[tokio::test]
async fn test_default_kcl_file_for_dir_nested_main_kcl() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&dir).unwrap();
std::fs::create_dir_all(dir.join("assembly")).unwrap();
std::fs::write(dir.join("assembly").join("main.kcl"), vec![]).unwrap();
let file = crate::settings::utils::walk_dir(&dir).await.unwrap();
let default_file = super::get_default_kcl_file_for_dir(&dir, file).await.unwrap();
assert_eq!(
default_file,
dir.join("assembly").join("main.kcl").display().to_string()
);
std::fs::remove_dir_all(dir).unwrap();
}
#[tokio::test]
async fn test_default_kcl_file_for_dir_nested_thing_kcl() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&dir).unwrap();
std::fs::create_dir_all(dir.join("assembly")).unwrap();
std::fs::write(dir.join("assembly").join("thing.kcl"), vec![]).unwrap();
let file = crate::settings::utils::walk_dir(&dir).await.unwrap();
let default_file = super::get_default_kcl_file_for_dir(&dir, file).await.unwrap();
assert_eq!(
default_file,
dir.join("assembly").join("thing.kcl").display().to_string()
);
std::fs::remove_dir_all(dir).unwrap();
}
#[tokio::test]
async fn test_rename_project_directory_empty_dir() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&dir).unwrap();
let new_name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let new_dir = super::rename_project_directory(&dir, &new_name).await.unwrap();
assert_eq!(new_dir, std::env::temp_dir().join(&new_name));
std::fs::remove_dir_all(new_dir).unwrap();
}
#[tokio::test]
async fn test_rename_project_directory_empty_name() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&dir).unwrap();
let result = super::rename_project_directory(&dir, "").await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), "New name for project cannot be empty");
std::fs::remove_dir_all(dir).unwrap();
}
#[tokio::test]
async fn test_rename_project_directory_non_empty_dir() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("main.kcl"), vec![]).unwrap();
let new_name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let new_dir = super::rename_project_directory(&dir, &new_name).await.unwrap();
assert_eq!(new_dir, std::env::temp_dir().join(&new_name));
std::fs::remove_dir_all(new_dir).unwrap();
}
#[tokio::test]
async fn test_rename_project_directory_non_empty_dir_recursive() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&dir).unwrap();
std::fs::create_dir_all(dir.join("assembly")).unwrap();
std::fs::write(dir.join("assembly").join("main.kcl"), vec![]).unwrap();
let new_name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let new_dir = super::rename_project_directory(&dir, &new_name).await.unwrap();
assert_eq!(new_dir, std::env::temp_dir().join(&new_name));
std::fs::remove_dir_all(new_dir).unwrap();
}
#[tokio::test]
async fn test_rename_project_directory_dir_is_file() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let dir = std::env::temp_dir().join(&name);
std::fs::write(&dir, vec![]).unwrap();
let new_name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let result = super::rename_project_directory(&dir, &new_name).await;
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
format!("Path `{}` is not a directory", dir.display())
);
std::fs::remove_file(dir).unwrap();
}
#[tokio::test]
async fn test_rename_project_directory_new_name_exists() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&dir).unwrap();
let new_name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let new_dir = std::env::temp_dir().join(&new_name);
std::fs::create_dir_all(&new_dir).unwrap();
let result = super::rename_project_directory(&dir, &new_name).await;
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
format!(
"Path `{}` already exists, cannot rename to an existing path",
new_dir.display()
)
);
std::fs::remove_dir_all(new_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_source_path_dot() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
std::env::set_current_dir(&tmp_project_dir).unwrap();
let state = super::ProjectState::new_from_path(std::path::PathBuf::from("."))
.await
.unwrap();
assert_eq!(state.project.file.name, name);
assert_eq!(
state
.project
.file
.path
.trim_start_matches("/private"),
tmp_project_dir.display().to_string()
);
assert_eq!(
state
.current_file
.unwrap()
.trim_start_matches("/private"),
tmp_project_dir.join("main.kcl").display().to_string()
);
assert_eq!(
state
.project
.default_file
.trim_start_matches("/private"),
tmp_project_dir.join("main.kcl").display().to_string()
);
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_main_kcl_not_exists() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
let state = super::ProjectState::new_from_path(tmp_project_dir.clone())
.await
.unwrap();
assert_eq!(state.project.file.name, name);
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
assert_eq!(
state.current_file,
Some(tmp_project_dir.join("main.kcl").display().to_string())
);
assert_eq!(
state.project.default_file,
tmp_project_dir.join("main.kcl").display().to_string()
);
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_main_kcl_exists() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
std::fs::write(tmp_project_dir.join("main.kcl"), vec![]).unwrap();
let state = super::ProjectState::new_from_path(tmp_project_dir.clone())
.await
.unwrap();
assert_eq!(state.project.file.name, name);
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
assert_eq!(
state.current_file,
Some(tmp_project_dir.join("main.kcl").display().to_string())
);
assert_eq!(
state.project.default_file,
tmp_project_dir.join("main.kcl").display().to_string()
);
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_explicit_open_main_kcl() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
std::fs::write(tmp_project_dir.join("main.kcl"), vec![]).unwrap();
let state = super::ProjectState::new_from_path(tmp_project_dir.join("main.kcl"))
.await
.unwrap();
assert_eq!(state.project.file.name, name);
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
assert_eq!(
state.current_file,
Some(tmp_project_dir.join("main.kcl").display().to_string())
);
assert_eq!(
state.project.default_file,
tmp_project_dir.join("main.kcl").display().to_string()
);
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_explicit_open_thing_kcl() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
std::fs::write(tmp_project_dir.join("thing.kcl"), vec![]).unwrap();
let state = super::ProjectState::new_from_path(tmp_project_dir.join("thing.kcl"))
.await
.unwrap();
assert_eq!(state.project.file.name, name);
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
assert_eq!(
state.current_file,
Some(tmp_project_dir.join("thing.kcl").display().to_string())
);
assert_eq!(
state.project.default_file,
tmp_project_dir.join("thing.kcl").display().to_string()
);
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_explicit_open_model_obj() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
std::fs::write(tmp_project_dir.join("model.obj"), vec![]).unwrap();
let state = super::ProjectState::new_from_path(tmp_project_dir.join("model.obj"))
.await
.unwrap();
assert_eq!(state.project.file.name, name);
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
assert_eq!(
state.current_file,
Some(tmp_project_dir.join("model.obj.kcl").display().to_string())
);
assert_eq!(
state.project.default_file,
tmp_project_dir.join("model.obj.kcl").display().to_string()
);
let kcl_file_contents = tokio::fs::read(tmp_project_dir.join("model.obj.kcl")).await.unwrap();
assert_eq!(
String::from_utf8_lossy(&kcl_file_contents),
r#"// This file was automatically generated by the application when you
// double-clicked on the model file.
// You can edit this file to add your own content.
// But we recommend you keep the import statement as it is.
// For more information on the import statement, see the documentation at:
// https://zoo.dev/docs/kcl/import
const model = import("model.obj")"#
);
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_explicit_open_settings_toml() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
std::fs::write(tmp_project_dir.join("settings.toml"), vec![]).unwrap();
let result = super::ProjectState::new_from_path(tmp_project_dir.join("settings.toml")).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), format!("File type (toml) cannot be opened with this app: `{}`, try opening one of the following file types: stp, glb, fbxb, fbx, gltf, obj, ply, sldprt, step, stl, kcl", tmp_project_dir.join("settings.toml").display()));
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_explicit_open_non_relevant_file() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
std::fs::write(tmp_project_dir.join("settings.docx"), vec![]).unwrap();
let result = super::ProjectState::new_from_path(tmp_project_dir.join("settings.docx")).await;
assert!(result.is_err());
assert_eq!(result.unwrap_err().to_string(), format!("File type (docx) cannot be opened with this app: `{}`, try opening one of the following file types: stp, glb, fbxb, fbx, gltf, obj, ply, sldprt, step, stl, kcl", tmp_project_dir.join("settings.docx").display()));
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_explicit_open_no_file_extension() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
std::fs::write(tmp_project_dir.join("file"), vec![]).unwrap();
let result = super::ProjectState::new_from_path(tmp_project_dir.join("file")).await;
assert!(result.is_err());
assert_eq!(
result.unwrap_err().to_string(),
format!(
"Error getting the extension of the file: `{}`",
tmp_project_dir.join("file").display()
)
);
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_explicit_open_file_with_space_kcl() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
std::fs::write(tmp_project_dir.join("i have a space.kcl"), vec![]).unwrap();
let state = super::ProjectState::new_from_path(tmp_project_dir.join("i have a space.kcl"))
.await
.unwrap();
assert_eq!(state.project.file.name, name);
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
assert_eq!(
state.current_file,
Some(tmp_project_dir.join("i have a space.kcl").display().to_string())
);
assert_eq!(
state.project.default_file,
tmp_project_dir.join("i have a space.kcl").display().to_string()
);
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
#[tokio::test]
async fn test_project_state_new_from_path_explicit_open_file_with_space_kcl_url_encoded() {
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
let tmp_project_dir = std::env::temp_dir().join(&name);
std::fs::create_dir_all(&tmp_project_dir).unwrap();
std::fs::write(tmp_project_dir.join("i have a space.kcl"), vec![]).unwrap();
let state = super::ProjectState::new_from_path(tmp_project_dir.join("i%20have%20a%20space.kcl"))
.await
.unwrap();
assert_eq!(state.project.file.name, name);
assert_eq!(state.project.file.path, tmp_project_dir.display().to_string());
assert_eq!(
state.current_file,
Some(tmp_project_dir.join("i have a space.kcl").display().to_string())
);
assert_eq!(
state.project.default_file,
tmp_project_dir.join("i have a space.kcl").display().to_string()
);
std::fs::remove_dir_all(tmp_project_dir).unwrap();
}
}