-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
257 additions
and
5 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ¬_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 ¬_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"]); | ||
} |