diff options
Diffstat (limited to 'mling/src/proj_mgr')
| -rw-r--r-- | mling/src/proj_mgr/checklist_reader.rs | 294 | ||||
| -rw-r--r-- | mling/src/proj_mgr/generator.rs | 57 | ||||
| -rw-r--r-- | mling/src/proj_mgr/metadata.rs | 142 | ||||
| -rw-r--r-- | mling/src/proj_mgr/mod.rs | 30 | ||||
| -rw-r--r-- | mling/src/proj_mgr/show_binaries.rs | 78 | ||||
| -rw-r--r-- | mling/src/proj_mgr/show_directories.rs | 61 |
6 files changed, 0 insertions, 662 deletions
diff --git a/mling/src/proj_mgr/checklist_reader.rs b/mling/src/proj_mgr/checklist_reader.rs deleted file mode 100644 index 558d303..0000000 --- a/mling/src/proj_mgr/checklist_reader.rs +++ /dev/null @@ -1,294 +0,0 @@ -use std::{collections::HashMap, io, path::Path}; - -/// Reads and parses a `CHECKLIST.md` file, extracting both *values* (from -/// fenced code blocks) and *toggles* (from checkbox lines). -/// -/// # Format -/// -/// - **Values**: fenced code blocks where the info string is the key and -/// the first non-empty line is the value. Example: -/// <code>\`\`\`name<br>my-cli<br>\`\`\`</code> -/// - **Toggles**: checkbox lines like `- [x] \`ns:key\`` (checked) or -/// `- [ ] \`ns:key\`` (unchecked). -/// -/// Checked items are stored as `"namespace:key"`. -/// -/// # Usage -/// -/// ```rust,ignore -/// let reader = CheckListReader::from(&Path::new("CHECKLIST.md")).unwrap(); -/// assert_eq!(reader.read_value("name"), Some("my-cli".into())); -/// assert!(reader.read_toggle("ver:0.2")); -/// assert!(!reader.read_toggle("feat:comp")); -/// assert_eq!(reader.read_toggles("feat"), vec!["feat:async"]); -/// ``` -pub struct CheckListReader { - /// Values extracted from fenced code blocks: `key → value`. - values: HashMap<String, String>, - - /// Checked toggles: `"namespace:key" → true` (only checked items are stored). - toggles: HashMap<String, bool>, - - /// All toggles (checked or not): `"namespace:key" → checked`. - all_toggles: HashMap<String, bool>, -} - -impl CheckListReader { - /// Parse a CHECKLIST.md from a file path in a single left-to-right pass - pub fn from(path: &Path) -> Result<Self, io::Error> { - let content = std::fs::read_to_string(path)?; - - let mut reader = Self { - values: HashMap::new(), - toggles: HashMap::new(), - all_toggles: HashMap::new(), - }; - reader.parse(&content); - Ok(reader) - } - - /// Parse CHECKLIST.md content in a single pass. - fn parse(&mut self, content: &str) { - let lines: Vec<&str> = content.lines().collect(); - let mut i = 0; - - while i < lines.len() { - let line = lines[i]; - - if let Some(key) = line.strip_prefix("```").map(|s| s.trim()) - && !key.is_empty() - && !key.starts_with(' ') - { - let mut block_lines = Vec::new(); - i += 1; - while i < lines.len() && !lines[i].trim_start().starts_with("```") { - block_lines.push(lines[i]); - i += 1; - } - let value = block_lines - .into_iter() - .find(|l| !l.trim().is_empty()) - .map(|l| l.trim().to_string()); - if let Some(val) = value { - self.values.insert(key.to_string(), val); - } - continue; - } - - if let Some(toggle_key) = Self::parse_toggle_line(line) { - let is_checked = line.contains("[x]") || line.contains("[X]"); - self.all_toggles.insert(toggle_key.clone(), is_checked); - if is_checked { - self.toggles.insert(toggle_key, true); - } - } - - i += 1; - } - } - - /// Extract the `namespace:key` from a toggle line. - /// - /// Matches: `- [x] `namespace:key`` or `- [ ] `namespace:key`` - fn parse_toggle_line(line: &str) -> Option<String> { - let line = line.trim(); - if !line.starts_with("- [") { - return None; - } - // Find the backtick-enclosed key - let start = line.find('`')?; - let rest = &line[start + 1..]; - let end = rest.find('`')?; - let key = rest[..end].trim(); - if key.is_empty() { - return None; - } - Some(key.to_string()) - } - - /// Read a value by its key. - /// - /// Returns `Some(value)` if a fenced code block with that key was found, - /// or `None` if the key does not exist. - #[must_use] - pub fn read_value(&self, key: &str) -> Option<String> { - self.values.get(key).cloned() - } - - /// Check if a toggle (`namespace:key`) is checked. - /// - /// Returns `true` if the checkbox line was `- [x]`, `false` if it was - /// `- [ ]` or the key does not exist in the file. - #[must_use] - pub fn read_toggle(&self, key: &str) -> bool { - self.toggles.contains_key(key) - } - - /// Get all toggles under a given `namespace` that are checked. - /// - /// For example, `read_toggles("feat")` returns all checked keys starting - /// with `"feat:"`, such as `["feat:comp", "feat:parser"]`. - #[must_use] - pub fn read_toggles(&self, namespace: &str) -> Vec<String> { - let prefix = format!("{namespace}:"); - let mut keys: Vec<String> = self - .toggles - .keys() - .filter(|k| k.starts_with(&prefix)) - .cloned() - .collect(); - keys.sort(); - keys - } - - /// Get ALL toggles (checked or not) under a namespace. - /// - /// Useful for iterating all available options. - #[must_use] - pub fn all_toggles(&self, namespace: &str) -> Vec<String> { - let prefix = format!("{namespace}:"); - let mut keys: Vec<String> = self - .all_toggles - .keys() - .filter(|k| k.starts_with(&prefix)) - .cloned() - .collect(); - keys.sort(); - keys - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn sample_md() -> &'static str { - r#"> Some intro - -## Question 1: What is your project name? - -```name -my-cli -``` - -## Question 2: Which version? - -- [x] `ver:0.2` - -## Question 3: Features? - -- [ ] `feat:structural_renderer` -- [x] `feat:comp` -- [x] `feat:parser` -- [ ] `feat:async` - -```other -some value -``` -"# - } - - #[test] - fn test_read_value() { - let mut reader = CheckListReader { - values: HashMap::new(), - toggles: HashMap::new(), - all_toggles: HashMap::new(), - }; - reader.parse(sample_md()); - assert_eq!(reader.read_value("name"), Some("my-cli".into())); - assert_eq!(reader.read_value("other"), Some("some value".into())); - assert_eq!(reader.read_value("nonexistent"), None); - } - - #[test] - fn test_read_toggle() { - let mut reader = CheckListReader { - values: HashMap::new(), - toggles: HashMap::new(), - all_toggles: HashMap::new(), - }; - reader.parse(sample_md()); - assert!(reader.read_toggle("ver:0.2")); - assert!(reader.read_toggle("feat:comp")); - assert!(reader.read_toggle("feat:parser")); - assert!(!reader.read_toggle("feat:structural_renderer")); - assert!(!reader.read_toggle("feat:async")); - assert!(!reader.read_toggle("nonexistent:key")); - } - - #[test] - fn test_read_toggles() { - let mut reader = CheckListReader { - values: HashMap::new(), - toggles: HashMap::new(), - all_toggles: HashMap::new(), - }; - reader.parse(sample_md()); - let feat_toggles = reader.read_toggles("feat"); - assert_eq!(feat_toggles, vec!["feat:comp", "feat:parser"]); - } - - #[test] - fn test_all_toggles() { - let mut reader = CheckListReader { - values: HashMap::new(), - toggles: HashMap::new(), - all_toggles: HashMap::new(), - }; - reader.parse(sample_md()); - let all = reader.all_toggles("feat"); - assert_eq!( - all, - vec![ - "feat:async", - "feat:comp", - "feat:parser", - "feat:structural_renderer", - ] - ); - } - - #[test] - fn test_from_path() { - // Write a temp CHECKLIST.md, read it, then clean up - let dir = std::env::temp_dir().join("mling_checklist_test"); - std::fs::create_dir_all(&dir).unwrap(); - let path = dir.join("CHECKLIST.md"); - std::fs::write(&path, sample_md()).unwrap(); - - let reader = CheckListReader::from(&path).unwrap(); - assert_eq!(reader.read_value("name"), Some("my-cli".into())); - assert!(reader.read_toggle("ver:0.2")); - - let _ = std::fs::remove_dir_all(&dir); - } - - #[test] - fn test_empty_file() { - let mut reader = CheckListReader { - values: HashMap::new(), - toggles: HashMap::new(), - all_toggles: HashMap::new(), - }; - reader.parse(""); - assert_eq!(reader.read_value("anything"), None); - assert!(!reader.read_toggle("ns:key")); - assert!(reader.read_toggles("ns").is_empty()); - } - - #[test] - fn test_no_toggle_or_value_lines() { - let md = "# Just a heading\n\nSome text\n\n```code\nstill not a value\n```\n"; - let mut reader = CheckListReader { - values: HashMap::new(), - toggles: HashMap::new(), - all_toggles: HashMap::new(), - }; - reader.parse(md); - // "code" isn't a valid value key (it's used as a language tag, not a name/value key) - // but our parser treats ANY ```key as a value block. This is correct behavior — - // malformed CHECKLIST.md may produce unexpected values. - assert_eq!(reader.read_value("code"), Some("still not a value".into())); - } -} diff --git a/mling/src/proj_mgr/generator.rs b/mling/src/proj_mgr/generator.rs deleted file mode 100644 index 4f965d9..0000000 --- a/mling/src/proj_mgr/generator.rs +++ /dev/null @@ -1,57 +0,0 @@ -use std::path::{self, PathBuf}; - -use mingling::{ - Groupped, RenderResult, - macros::{chain, pack, renderer, route}, -}; -use std::io::Write as _; - -use crate::{Next, proj_mgr::EntryGenerateProject, res::ResCurrentDir}; - -pack!(StateGenerateProjectReady = PathBuf); -pack!(ResultGenerateProjectChecklistCreated = PathBuf); - -pack!(StateGenerateProjectExecBegin = PathBuf); -pack!(StateGenerateProjectExecuting = ()); - -const CHECK_LIST_NAME: &str = "CHECKLIST.md"; -const CHECK_LIST_CONTENT: &str = include_str!("../../res/CHECKLIST.md"); - -#[chain] -pub fn handle_generate(_args: EntryGenerateProject, cwd: &ResCurrentDir) -> Next { - let checklist_path = cwd.path.join(CHECK_LIST_NAME); - - if !checklist_path.exists() { - StateGenerateProjectReady::new(checklist_path).to_chain() - } else { - StateGenerateProjectExecBegin::new(checklist_path).to_chain() - } -} - -#[chain] -pub fn handle_state_gen_proj_ready(prev: StateGenerateProjectReady) -> Next { - let path = prev.inner; - route!(std::fs::write(&path, CHECK_LIST_CONTENT)); - ResultGenerateProjectChecklistCreated::new(path).to_render() -} - -#[renderer] -pub fn render_gen_proj_checklist_created( - result: ResultGenerateProjectChecklistCreated, -) -> RenderResult { - let mut res = RenderResult::default(); - writeln!( - res, - "Successfully create {} at \"{}\"", - CHECK_LIST_NAME, - result.to_string_lossy() - ) - .ok(); - writeln!(res).ok(); - writeln!( - res, - "Please fill in {CHECK_LIST_NAME} and run `mling gen` again to continue generating" - ) - .ok(); - res -} diff --git a/mling/src/proj_mgr/metadata.rs b/mling/src/proj_mgr/metadata.rs deleted file mode 100644 index 1ba24e1..0000000 --- a/mling/src/proj_mgr/metadata.rs +++ /dev/null @@ -1,142 +0,0 @@ -use serde::Deserialize; -use serde::Serialize; - -use std::path::PathBuf; -use std::process::Command; - -/// Read cargo metadata by running `cargo metadata` with the given manifest path. -pub fn read_metadata(cargo_toml: &PathBuf) -> Result<CargoLockFile, std::io::Error> { - let output = Command::new("cargo") - .arg("metadata") - .arg("--format-version") - .arg("1") - .arg("--manifest-path") - .arg(cargo_toml) - .output()?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - return Err(std::io::Error::other(format!( - "cargo metadata failed: {}", - stderr - ))); - } - - let lock_file: CargoLockFile = serde_json::from_slice(&output.stdout) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; - - Ok(lock_file) -} - -/// A cargo metadata lock file that serde can serialize and deserialize. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CargoLockFile { - pub packages: Vec<Package>, - #[serde(rename = "workspace_members")] - pub workspace_members: Vec<String>, - #[serde(rename = "workspace_default_members")] - pub workspace_default_members: Vec<String>, - pub resolve: Resolve, - #[serde(rename = "target_directory")] - pub target_directory: String, - #[serde(rename = "build_directory")] - pub build_directory: String, - pub version: u64, - #[serde(rename = "workspace_root")] - pub workspace_root: String, - pub metadata: Option<serde_json::Value>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Package { - pub name: String, - pub version: String, - pub id: String, - pub license: Option<String>, - #[serde(rename = "license_file")] - pub license_file: Option<String>, - pub description: Option<String>, - pub source: Option<String>, - pub dependencies: Vec<Dependency>, - pub targets: Vec<Target>, - pub features: std::collections::BTreeMap<String, Vec<String>>, - #[serde(rename = "manifest_path")] - pub manifest_path: String, - pub metadata: Option<serde_json::Value>, - pub publish: Option<serde_json::Value>, - pub authors: Vec<String>, - pub categories: Vec<String>, - pub keywords: Vec<String>, - pub readme: Option<String>, - pub repository: Option<String>, - pub homepage: Option<String>, - pub documentation: Option<String>, - pub edition: String, - pub links: Option<String>, - #[serde(rename = "default_run")] - pub default_run: Option<String>, - #[serde(rename = "rust_version")] - pub rust_version: Option<String>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Dependency { - pub name: String, - pub source: Option<String>, - pub req: String, - pub kind: Option<String>, - pub rename: Option<String>, - pub optional: bool, - #[serde(rename = "uses_default_features")] - pub uses_default_features: bool, - pub features: Vec<String>, - pub target: Option<String>, - pub registry: Option<String>, - #[serde(skip_serializing_if = "Option::is_none")] - pub path: Option<String>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Target { - pub kind: Vec<String>, - #[serde(rename = "crate_types")] - pub crate_types: Vec<String>, - pub name: String, - #[serde(rename = "src_path")] - pub src_path: String, - pub edition: String, - pub doc: bool, - pub doctest: bool, - pub test: bool, - #[serde(skip_serializing_if = "Option::is_none")] - #[serde(rename = "required-features")] - pub required_features: Option<Vec<String>>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Resolve { - pub nodes: Vec<ResolveNode>, - pub root: Option<String>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ResolveNode { - pub id: String, - pub dependencies: Vec<String>, - pub deps: Vec<DepInfo>, - pub features: Vec<String>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DepInfo { - pub name: String, - pub pkg: String, - #[serde(rename = "dep_kinds")] - pub dep_kinds: Vec<DepKind>, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DepKind { - pub kind: Option<String>, - pub target: Option<String>, -} diff --git a/mling/src/proj_mgr/mod.rs b/mling/src/proj_mgr/mod.rs deleted file mode 100644 index e0a4216..0000000 --- a/mling/src/proj_mgr/mod.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::ThisProgram; -use mingling::{ - Program, - macros::{dispatcher, program_setup}, -}; - -pub mod checklist_reader; -pub mod generator; -pub mod metadata; -pub mod show_binaries; -pub mod show_directories; - -dispatcher!("gen", CMDGenerateProject => EntryGenerateProject); - -dispatcher!("show.binaries"); -dispatcher!("show.workspace-dir", - CMDShowWorkspaceDirectory => EntryShowWorkspaceDirectory -); -dispatcher!("show.target-dir", - CMDShowTargetDirectories => EntryShowTargetDirectories -); - -#[program_setup] -pub fn project_manager_setup(p: &mut Program<ThisProgram>) { - p.with_dispatcher(CMDGenerateProject); - - p.with_dispatcher(CMDShowBinaries); - p.with_dispatcher(CMDShowWorkspaceDirectory); - p.with_dispatcher(CMDShowTargetDirectories); -} diff --git a/mling/src/proj_mgr/show_binaries.rs b/mling/src/proj_mgr/show_binaries.rs deleted file mode 100644 index 9d5caf0..0000000 --- a/mling/src/proj_mgr/show_binaries.rs +++ /dev/null @@ -1,78 +0,0 @@ -use std::path::PathBuf; - -use colored::Colorize; -use mingling::{ - Groupped, RenderResult, - macros::{chain, pack, renderer}, -}; -use serde::Serialize; -use std::io::Write as _; - -use crate::{ - Next, - proj_mgr::{ - EntryShowBinaries, - metadata::{CargoLockFile, read_metadata}, - }, - res::ResManifestPath, -}; - -#[derive(Serialize, Groupped)] -pub struct ResultBinaries { - pub binaries: Vec<DataBinary>, -} - -#[derive(Serialize)] -pub struct DataBinary { - pub name: String, - pub path: PathBuf, -} - -#[chain] -pub fn handle_show_binaries(_args: EntryShowBinaries, manifest_path: &ResManifestPath) -> Next { - let metadata = read_metadata(manifest_path.resolved()).unwrap(); - let CargoLockFile { - packages, - workspace_members, - .. - } = metadata; - - let binaries: Vec<DataBinary> = packages - .into_iter() - .filter(|pkg| workspace_members.contains(&pkg.id)) - .flat_map(|pkg| { - pkg.targets - .into_iter() - .filter(|target| target.kind.iter().any(|k| k == "bin")) - .map(move |target| DataBinary { - name: target.name, - path: PathBuf::from(pkg.manifest_path.clone()) - .parent() - .unwrap_or(&PathBuf::from(".")) - .join("src") - .join(&target.src_path), - }) - }) - .collect(); - - ResultBinaries { binaries }.to_render() -} - -#[renderer] -pub fn render_binaries(binaries: ResultBinaries) -> RenderResult { - let mut result = RenderResult::default(); - writeln!(result, "{}", "Binaries:".bright_cyan().bold()).ok(); - if let Some(max_name_len) = binaries.binaries.iter().map(|b| b.name.len()).max() { - for binary in &binaries.binaries { - writeln!( - result, - " {:width$} `{}`", - binary.name.bright_yellow().bold(), - binary.path.display().to_string().italic(), - width = max_name_len - ) - .ok(); - } - } - result -} diff --git a/mling/src/proj_mgr/show_directories.rs b/mling/src/proj_mgr/show_directories.rs deleted file mode 100644 index 7d7c074..0000000 --- a/mling/src/proj_mgr/show_directories.rs +++ /dev/null @@ -1,61 +0,0 @@ -use colored::Colorize; -use mingling::{ - Groupped, RenderResult, - macros::{chain, pack, renderer}, -}; -use serde::Serialize; -use std::io::Write as _; - -use crate::{ - Next, - proj_mgr::{EntryShowTargetDirectories, EntryShowWorkspaceDirectory, metadata::read_metadata}, - res::ResManifestPath, -}; - -#[derive(Serialize, Groupped)] -pub struct ResultWorkspaceDirectory { - pub path: String, -} - -#[derive(Serialize, Groupped)] -pub struct ResultTargetDirectory { - pub path: String, -} - -#[chain] -pub fn handle_show_workspace_directory( - _args: EntryShowWorkspaceDirectory, - manifest_path: &ResManifestPath, -) -> Next { - let metadata = read_metadata(manifest_path.resolved()).unwrap(); - ResultWorkspaceDirectory { - path: metadata.workspace_root, - } - .to_render() -} - -#[chain] -pub fn handle_show_target_directory( - _args: EntryShowTargetDirectories, - manifest_path: &ResManifestPath, -) -> Next { - let metadata = read_metadata(manifest_path.resolved()).unwrap(); - ResultTargetDirectory { - path: metadata.target_directory, - } - .to_render() -} - -#[renderer] -pub fn render_workspace_directory(prev: ResultWorkspaceDirectory) -> RenderResult { - let mut result = RenderResult::default(); - writeln!(result, "{}", prev.path.bright_cyan().bold()).ok(); - result -} - -#[renderer] -pub fn render_target_directory(prev: ResultTargetDirectory) -> RenderResult { - let mut result = RenderResult::default(); - writeln!(result, "{}", prev.path.bright_cyan().bold()).ok(); - result -} |
