Skip to content

Commit

Permalink
Start on parse_deser module
Browse files Browse the repository at this point in the history
  • Loading branch information
ysthakur committed Dec 23, 2023
1 parent bf40e87 commit 02d7619
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 5 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ strip = "debuginfo"
anyhow = "1.0"
clap = { version = "4.3", features = ["derive", "env"] }
env_logger = "0.10"
thiserror = "1.0"
log = "0.4"
regex = "1.9"

Expand Down
8 changes: 6 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,22 @@ pub mod parse_man;
use serde::{Deserialize, Serialize};

/// Flags parsed from a command, as well as its parsed subcommands
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Deserialize, Eq, Serialize, PartialEq)]
pub struct CommandInfo {
pub name: String,
pub flags: Vec<Flag>,
pub subcommands: Vec<CommandInfo>,
}

/// A parsed flag
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Deserialize, Eq, Serialize, PartialEq)]
pub struct Flag {
/// The different short and long forms of a flag
pub forms: Vec<String>,
/// Optional description for the flag
pub desc: Option<String>,
}

pub enum Error {

}
37 changes: 37 additions & 0 deletions src/parse_deser/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use std::io;

use kdl::KdlError;
use thiserror::Error;

use super::kdl::KdlDeserError;

#[derive(Debug, Error)]
pub enum Error {
#[error("{file_path} has no extension")]
NoExtension { file_path: String },
#[error("{file_path} has an unrecognizable extension")]
UnrecognizableExtension { file_path: String },
#[error("error encountered while reading {file_path}")]
Io {
file_path: String,
#[source]
source: io::Error,
},
#[error("error encountered while deserializing {file_path}")]
Deser {
file_path: String,
#[source]
source: DeserError,
},
}

/// An error encountered while deserializing
#[derive(Debug, Error)]
pub enum DeserError {
#[error(transparent)]
Json(#[from] serde_json::Error),
#[error(transparent)]
Yaml(#[from] serde_yaml::Error),
#[error(transparent)]
Kdl(#[from] KdlDeserError),
}
49 changes: 49 additions & 0 deletions src/parse_deser/kdl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//! For deserializing from KDL, because the serde support is not great

use kdl::KdlDocument;
use thiserror::Error;

use crate::CommandInfo;

/// An error encountered when deserializing KDL specifically
#[derive(Debug, Error)]
pub enum KdlDeserError {
#[error(transparent)]
ParseError(#[from] kdl::KdlError),
}

type Result<T> = std::result::Result<T, KdlDeserError>;

pub fn parse_from_str(text: &str) -> Result<CommandInfo> {
kdl_to_cmd_info(text.parse()?)
}

fn kdl_to_cmd_info(doc: KdlDocument) -> Result<CommandInfo> {
todo!()
}

#[cfg(test)]
mod tests {
use super::parse_from_str;
use crate::CommandInfo;

#[test]
fn test1() {
assert_eq!(
CommandInfo {
name: "foo".to_string(),
flags: vec![],
subcommands: vec![]
},
parse_from_str(r#"
foo {
flags {
- "--help" "-h" {
"Show help output"
}
}
}
"#).unwrap()
)
}
}
52 changes: 49 additions & 3 deletions src/parse_deser/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,53 @@
//! For parsing completions from a serialization language (KDL, JSON, or YAML)

use std::path::Path;
pub mod error;
mod kdl;

pub fn parse(_file: impl AsRef<Path>) {
todo!()
use std::{fs, path::Path};

use self::error::DeserError;
use crate::{parse_deser::error::Error, CommandInfo};

pub type Result<T> = std::result::Result<T, Error>;

pub fn parse(file: impl AsRef<Path>) -> Result<CommandInfo> {
let file = file.as_ref();
let file_path = file.to_string_lossy().to_string();
if let Some(ext) = file.extension() {
match fs::read_to_string(file) {
Ok(text) => {
if let Some(ext) = ext.to_str() {
match parse_from_str(&text, ext) {
Ok(Some(cmd_info)) => Ok(cmd_info),
Ok(None) => Err(Error::UnrecognizableExtension { file_path }),
Err(e) => Err(Error::Deser {
file_path,
source: e,
}),
}
} else {
Err(Error::UnrecognizableExtension { file_path })
}
}
Err(e) => Err(Error::Io {
file_path,
source: e,
}),
}
} else {
Err(Error::NoExtension { file_path })
}
}

pub fn parse_from_str(
text: &str,
ext: &str,
) -> std::result::Result<Option<CommandInfo>, DeserError> {
let cmd_info = match ext {
"json" => Some(serde_json::from_str(&text)?),
"yaml" | "yml" => Some(serde_yaml::from_str(&text)?),
"kdl" => Some(kdl::parse_from_str(&text)?),
_ => None,
};
Ok(cmd_info)
}
114 changes: 114 additions & 0 deletions tests/gen_integration_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//! Test generating completions from JSON files

use std::{
env, fs,
path::PathBuf,
process::{Command, Stdio},
};

use assert_cmd::prelude::{CommandCargoExt, OutputAssertExt};

const BIN_NAME: &str = "man-completions";

fn run_test(shell: &str, outputs: &[&str], args: &[&str]) {
// The project's root directory
let root = env::var("CARGO_MANIFEST_DIR").unwrap();

let test_resources = PathBuf::from(root).join("tests/resources/gen");
let in_dir = test_resources.join("in");
let expected_dir = test_resources.join("expected");

let out_dir = tempfile::tempdir().unwrap();

// The man-completions binary to test
let mut cmd = Command::cargo_bin(BIN_NAME).unwrap();
let cmd = cmd.env("MANPATH", &in_dir).args(args).args([
"--out",
&out_dir.path().display().to_string(),
"--shell",
shell,
]);
// So we can explicitly ask for logging
if let Ok(log_level) = env::var("RUST_LOG") {
cmd.env("RUST_LOG", log_level).stderr(Stdio::inherit());
}
cmd.assert().success();

// Files that didn't get generated
let mut not_generated = Vec::new();
// Files that don't match the expected contents
let mut not_match = Vec::new();

for file_name in outputs {
let file_name = match shell {
"zsh" => format!("_{file_name}.zsh"),
"bash" => format!("_{file_name}.bash"),
"nu" => format!("{file_name}-completions.nu"),
"json" => format!("{file_name}.json"),
_ => todo!(),
};

let exp_file = expected_dir.join(&file_name);
let got_file = out_dir.path().join(&file_name);
if !got_file.exists() {
not_generated.push(file_name);
continue;
}

if exp_file.exists() {
let expected = fs::read(exp_file).unwrap();
let got = fs::read(&got_file).unwrap();
if expected != got {
not_match.push(file_name);
continue;
}
} else {
println!("No {file_name} found in expected folder");
not_match.push(file_name);
continue;
}

// Delete outputted file if it succeeded, since we don't need it anymore
fs::remove_file(got_file).unwrap();
}

if !not_generated.is_empty() {
println!("The following files weren't generated:");
for file_name in &not_generated {
println!("- {file_name}");
}
}

if !not_match.is_empty() {
// Make a tmp folder to copy the incorrect outputs to, to view later
let failed_dir = test_resources.join("tmp");
if !failed_dir.exists() {
fs::create_dir(&failed_dir).unwrap();
}

println!("The following files didn't match what was expected:");
for file_name in &not_match {
let exp = expected_dir.join(file_name);
let exp = exp.to_string_lossy();

// Copy the incorrect output out of the temp directory
let saved = failed_dir.join(file_name);
let got = fs::read(&out_dir.path().join(file_name)).unwrap();
fs::write(&saved, got).unwrap();

let saved = saved.display().to_string();
println!("Test for {file_name} failed: contents of {file_name} differed from expected");
println!("To see the diff, run `diff {exp} {saved}`");
println!("To overwrite the expected file, run `cp {saved} {exp}`");
}
}

out_dir.close().unwrap();

assert!(not_generated.is_empty() && not_match.is_empty());
}

#[test]
fn test1_zsh() {
run_test("zsh", &["test1"], &["--cmds", "^test1"]);
}

0 comments on commit 02d7619

Please sign in to comment.