use crate::prelude::*;
pub trait ExecInstance: Sized {
const BINARY_NAME: &'static str;
unsafe fn new_unchecked(exec_path: impl AsRef<Utf8Path>) -> Self;
fn get_inner_exec_path(&self) -> &Utf8Path;
fn bossy_command(&self) -> bossy::Command {
bossy::Command::pure(self.get_inner_exec_path())
}
fn version_command(&self) -> bossy::Command {
self.bossy_command().with_arg("--version")
}
fn validate_version(&self) -> std::result::Result<bool, bossy::Error> {
self
.version_command()
.run_and_wait_for_output()
.map(|status| status.success())
}
fn from_path(path: impl AsRef<Utf8Path>) -> Result<Self> {
let path = path.as_ref();
match path.try_exists() {
Ok(true) => Ok(unsafe { Self::new_unchecked(path) }),
Ok(false) => Err(Error::PathDoesNotExist {
path: path.to_owned(),
err: None,
}),
Err(e) => Err(Error::PathDoesNotExist {
path: path.to_owned(),
err: Some(e),
}),
}
}
fn new() -> Result<Self> {
let path = which::which(Self::BINARY_NAME)?;
let path = Utf8PathBuf::try_from(path)?;
let instance = unsafe { Self::new_unchecked(path) };
match instance.validate_version() {
Ok(true) => Ok(instance),
Ok(false) => Err(Error::VersionCheckFailed(None)),
Err(e) => Err(Error::VersionCheckFailed(Some(e))),
}
}
}
pub trait ExecChild<'src>: Sized {
const SUBCOMMAND_NAME: &'static str;
type Parent: ExecInstance;
unsafe fn new_unchecked(parent: &'src Self::Parent) -> Self;
fn get_inner_parent(&self) -> &Self::Parent;
fn bossy_command(&self) -> bossy::Command {
self
.get_inner_parent()
.bossy_command()
.with_arg(Self::SUBCOMMAND_NAME)
}
fn version_command(&self) -> bossy::Command {
self.bossy_command().with_arg("--version")
}
fn validate_version(&self) -> std::result::Result<bool, bossy::Error> {
self
.version_command()
.run_and_wait_for_output()
.map(|status| status.success())
}
fn new(parent: &'src Self::Parent) -> Result<Self> {
let instance = unsafe { Self::new_unchecked(parent) };
match instance.validate_version() {
Ok(true) => Ok(instance),
Ok(false) => Err(Error::VersionCheckFailed(None)),
Err(e) => Err(Error::VersionCheckFailed(Some(e))),
}
}
}
macro_rules! impl_exec_instance {
($t:ty, $name:expr) => {
impl $crate::shared::ExecInstance for $t {
const BINARY_NAME: &'static str = $name;
unsafe fn new_unchecked(exec_path: impl AsRef<::camino::Utf8Path>) -> Self {
Self {
exec_path: exec_path.as_ref().to_path_buf(),
}
}
fn get_inner_exec_path(&self) -> &::camino::Utf8Path {
&self.exec_path
}
}
impl $t {
pub fn new() -> $crate::error::Result<Self> {
$crate::shared::ExecInstance::new()
}
}
};
($t:ty, $name:expr, skip_version_check) => {
impl $crate::shared::ExecInstance for $t {
const BINARY_NAME: &'static str = $name;
unsafe fn new_unchecked(exec_path: impl AsRef<::camino::Utf8Path>) -> Self {
Self {
exec_path: exec_path.as_ref().to_path_buf(),
}
}
fn get_inner_exec_path(&self) -> &::camino::Utf8Path {
&self.exec_path
}
fn validate_version(&self) -> std::result::Result<bool, bossy::Error> {
Ok(true)
}
}
impl $t {
pub fn new() -> $crate::error::Result<Self> {
$crate::shared::ExecInstance::new()
}
}
};
($t:ty, $name:expr, skip_version_check, extra_flags = $extra_flags:expr) => {
impl $crate::shared::ExecInstance for $t {
const BINARY_NAME: &'static str = $name;
unsafe fn new_unchecked(exec_path: impl AsRef<::camino::Utf8Path>) -> Self {
Self {
exec_path: exec_path.as_ref().to_path_buf(),
}
}
fn get_inner_exec_path(&self) -> &::camino::Utf8Path {
&self.exec_path
}
fn bossy_command(&self) -> ::bossy::Command {
bossy::Command::pure(&self.get_inner_exec_path()).with_args($extra_flags)
}
fn validate_version(&self) -> std::result::Result<bool, bossy::Error> {
Ok(true)
}
}
impl $t {
pub fn new() -> $crate::error::Result<Self> {
$crate::shared::ExecInstance::new()
}
}
};
}
pub(crate) use impl_exec_instance;
macro_rules! impl_exec_child {
($t:ty, parent = $parent:ty, subcommand = $name:expr) => {
impl<'src> $crate::shared::ExecChild<'src> for $t {
const SUBCOMMAND_NAME: &'static str = $name;
type Parent = $parent;
unsafe fn new_unchecked(parent: &'src Self::Parent) -> Self {
Self {
exec_parent: parent,
}
}
fn get_inner_parent(&self) -> &Self::Parent {
&self.exec_parent
}
}
impl<'src> $t {
pub fn new(
parent: &'src <Self as $crate::shared::ExecChild<'src>>::Parent,
) -> $crate::error::Result<Self> {
$crate::shared::ExecChild::new(parent)
}
}
};
}
pub(crate) use impl_exec_child;
pub(crate) trait NomFromStr: Sized {
fn nom_from_str(input: &str) -> IResult<&str, Self>;
}
impl NomFromStr for NonZeroU8 {
#[tracing::instrument(level = "trace", skip(input))]
fn nom_from_str(input: &str) -> IResult<&str, Self> {
map_res(digit1, |s: &str| s.parse())(input)
}
}
#[allow(private_bounds)] pub trait PublicCommandOutput: std::fmt::Debug + Serialize + CommandNomParsable {
type PrimarySuccess: std::fmt::Debug;
fn success(&self) -> Result<&Self::PrimarySuccess>;
fn successful(&self) -> bool {
self.success().is_ok()
}
fn failed(&self) -> bool {
!self.successful()
}
}
pub(crate) trait CommandNomParsable: Sized + std::fmt::Debug {
fn success_unimplemented(stdout: String) -> Self;
fn error_unimplemented(stderr: String) -> Self;
fn success_nom_from_str(input: &str) -> IResult<&str, Self> {
map(rest, |s: &str| Self::success_unimplemented(s.to_owned()))(input)
}
fn success_from_str(input: &str) -> Self {
match Self::success_nom_from_str(input) {
Ok((remaining, output)) => {
if !remaining.is_empty() {
tracing::warn!(?remaining, ?output, "Remaining input after parsing a command success output. This likely indicates a potential improvement to the output parser. PRs welcome!")
}
output
}
Err(err) => {
error!(?err, "Failed to parse success output");
Self::success_unimplemented(input.to_owned())
}
}
}
fn errored_nom_from_str(input: &str) -> IResult<&str, Self> {
map(rest, |s: &str| Self::error_unimplemented(s.to_owned()))(input)
}
fn errored_from_str(input: &str) -> Self {
match Self::errored_nom_from_str(input) {
Ok((remaining, output)) => {
if !remaining.is_empty() {
warn!(?remaining, ?output, "Remaining input after parsing a command error output. This likely indicates a potential improvement to the output parser. PRs welcome!")
}
output
}
Err(err) => {
tracing::error!(?err, "Failed to parse error output");
Self::error_unimplemented(input.to_owned())
}
}
}
fn from_success_stream(success: bool, string: String) -> Self {
if success {
Self::success_from_str(&string)
} else {
Self::errored_from_str(&string)
}
}
fn from_bossy_result(result: bossy::Result<Output>) -> Result<Self> {
let (success, string) = match result {
Ok(output) => (true, output.stdout_str()?.to_owned()),
Err(err) => match err.output() {
Some(output) => (false, output.stderr_str()?.to_owned()),
None => Err(Error::CannotLocateStderrStream { err })?,
},
};
Ok(Self::from_success_stream(success, string))
}
}
macro_rules! impl_from_str_nom {
($type:ty) => {
impl std::str::FromStr for $type {
type Err = $crate::error::Error;
#[tracing::instrument(level = "trace", skip(input))]
fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
match <$type>::nom_from_str(input) {
Ok((remaining, output)) => {
if !remaining.is_empty() {
tracing::warn!(?remaining, ?output, "Remaining input after parsing. This likely indicates a potential improvement to the output parser. PRs welcome!")
}
Ok(output)
}
Err(e) => Err(Error::NomParsingFailed {
err: e.to_owned(),
name: stringify!($type).into(),
}),
}
}
}
};
($type:ty, unimplemented = $unimplemented:expr) => {
$crate::nom_from_str!($type);
impl $crate::shared::SuccessfullyParsed for $type {
fn successfully_parsed(&self) -> bool {
matches!(self, $unimplemented)
}
}
};
}
pub(crate) use impl_from_str_nom;