enum dispatch

This commit is contained in:
figsoda 2022-12-30 12:33:44 -05:00
parent 6fc0d29d5e
commit 2f2248c9ff
10 changed files with 134 additions and 79 deletions

13
Cargo.lock generated
View File

@ -60,6 +60,18 @@ dependencies = [
"os_str_bytes",
]
[[package]]
name = "enum_dispatch"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1693044dcf452888dd3a6a6a0dab67f0652094e3920dfe029a54d2f37d9b7394"
dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "errno"
version = "0.2.8"
@ -167,6 +179,7 @@ version = "0.1.1"
dependencies = [
"anyhow",
"clap",
"enum_dispatch",
"indoc",
"serde",
"serde_json",

View File

@ -13,6 +13,7 @@ categories = ["command-line-utilities"]
[dependencies]
anyhow = "1.0.68"
enum_dispatch = "0.3.9"
indoc = "1.0.8"
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.91"

View File

@ -14,7 +14,7 @@ pub struct Opts {
/// specify the fetcher function instead of inferring from the URL
#[arg(short, long)]
pub fetcher: Option<Fetcher>,
pub fetcher: Option<FetcherFunction>,
/// extra indentation (in number of spaces)
#[arg(short, long, default_value_t = 0)]
@ -23,7 +23,7 @@ pub struct Opts {
#[derive(Clone, Debug, ValueEnum)]
#[clap(rename_all = "camelCase")]
pub enum Fetcher {
pub enum FetcherFunction {
FetchFromGitHub,
FetchFromGitLab,
FetchFromSourcehut,

View File

@ -1,6 +1,7 @@
use crate::fetcher::UrlFlakeFetcher;
use crate::{fetcher::UrlFlakeFetcher, impl_fetcher};
pub struct Fetchgit;
impl_fetcher!(Fetchgit);
impl UrlFlakeFetcher for Fetchgit {
const FLAKE_TYPE: &'static str = "git";

View File

@ -1,12 +1,13 @@
use crate::fetcher::SimpleFlakeFetcher;
use crate::{fetcher::SimpleFlakeFetcher, impl_fetcher};
pub struct FetchFromGitHub<'a>(pub Option<&'a str>);
pub struct FetchFromGitHub(pub Option<String>);
impl_fetcher!(FetchFromGitHub);
impl<'a> SimpleFlakeFetcher<'a> for FetchFromGitHub<'a> {
impl<'a> SimpleFlakeFetcher<'a> for FetchFromGitHub {
const FLAKE_TYPE: &'static str = "github";
const NAME: &'static str = "fetchFromGitHub";
fn host(&self) -> Option<&'a str> {
self.0
fn host(&'a self) -> &'a Option<String> {
&self.0
}
}

View File

@ -1,12 +1,13 @@
use crate::fetcher::SimpleFlakeFetcher;
use crate::{fetcher::SimpleFlakeFetcher, impl_fetcher};
pub struct FetchFromGitLab<'a>(pub Option<&'a str>);
pub struct FetchFromGitLab(pub Option<String>);
impl_fetcher!(FetchFromGitLab);
impl<'a> SimpleFlakeFetcher<'a> for FetchFromGitLab<'a> {
impl<'a> SimpleFlakeFetcher<'a> for FetchFromGitLab {
const FLAKE_TYPE: &'static str = "gitlab";
const NAME: &'static str = "fetchFromGitLab";
fn host(&self) -> Option<&'a str> {
self.0
fn host(&'a self) -> &'a Option<String> {
&self.0
}
}

View File

@ -1,6 +1,7 @@
use crate::fetcher::UrlFlakeFetcher;
use crate::{fetcher::UrlFlakeFetcher, impl_fetcher};
pub struct Fetchhg;
impl_fetcher!(Fetchhg);
impl UrlFlakeFetcher for Fetchhg {
const FLAKE_TYPE: &'static str = "hg";

View File

@ -4,6 +4,7 @@ mod gitlab;
mod hg;
mod sourcehut;
use enum_dispatch::enum_dispatch;
pub use git::Fetchgit;
pub use github::FetchFromGitHub;
pub use gitlab::FetchFromGitLab;
@ -20,47 +21,42 @@ use std::{
process::{Command, Output, Stdio},
};
#[enum_dispatch]
pub trait Fetcher {
fn fetch_nix(&self, out: &mut impl Write, url: &Url, rev: String, indent: &str) -> Result<()>;
}
trait GetStdout {
fn get_stdout(&mut self) -> Result<Vec<u8>>;
#[enum_dispatch(Fetcher)]
pub enum FetcherDispatch {
FetchFromGitHub(FetchFromGitHub),
FetchFromGitLab(FetchFromGitLab),
FetchFromSourcehut(FetchFromSourcehut),
Fetchgit(Fetchgit),
Fetchhg(Fetchhg),
}
impl GetStdout for Command {
fn get_stdout(&mut self) -> Result<Vec<u8>> {
let Output { stdout, status, .. } = self.stderr(Stdio::inherit()).output()?;
if !status.success() {
bail!("command exited with exit code {}", status);
#[macro_export]
macro_rules! impl_fetcher {
($t:ident $($tt:tt)*) => {
impl $($tt)* $crate::fetcher::Fetcher for $t $($tt)* {
fn fetch_nix(
&self,
out: &mut impl ::std::io::Write,
url: &::url::Url,
rev: String,
indent: &str,
) -> ::anyhow::Result<()> {
self.fetch_nix_imp(out, url, rev, indent)
}
}
Ok(stdout)
}
}
pub fn flake_prefetch(flake_ref: String) -> Result<String> {
#[derive(Deserialize)]
struct PrefetchOutput {
hash: String,
}
eprintln!("$ nix flake prefetch --json {flake_ref}");
Ok(serde_json::from_slice::<PrefetchOutput>(
&Command::new("nix")
.arg("flake")
.arg("prefetch")
.arg("--json")
.arg(flake_ref)
.get_stdout()?,
)?
.hash)
};
}
pub trait SimpleFlakeFetcher<'a> {
const FLAKE_TYPE: &'static str;
const NAME: &'static str;
fn host(&self) -> Option<&'a str>;
fn host(&'a self) -> &'a Option<String>;
fn get_repo(&self, url: &Url) -> Option<(String, String)> {
let mut xs = url.path_segments()?;
@ -72,7 +68,13 @@ pub trait SimpleFlakeFetcher<'a> {
))
}
fn fetch_nix(&self, out: &mut impl Write, url: &Url, rev: String, indent: &str) -> Result<()> {
fn fetch_nix_imp(
&'a self,
out: &mut impl Write,
url: &Url,
rev: String,
indent: &str,
) -> Result<()> {
let (owner, repo) = self
.get_repo(url)
.with_context(|| format!("failed to parse {url} as a {} url", Self::FLAKE_TYPE))?;
@ -107,7 +109,13 @@ pub trait UrlFlakeFetcher {
const FLAKE_TYPE: &'static str;
const NAME: &'static str;
fn fetch_nix(&self, out: &mut impl Write, url: &Url, rev: String, indent: &str) -> Result<()> {
fn fetch_nix_imp(
&self,
out: &mut impl Write,
url: &Url,
rev: String,
indent: &str,
) -> Result<()> {
let hash = flake_prefetch(format!(
"{}+{url}?{}={rev}",
Self::FLAKE_TYPE,
@ -128,3 +136,35 @@ pub trait UrlFlakeFetcher {
Ok(())
}
}
trait GetStdout {
fn get_stdout(&mut self) -> Result<Vec<u8>>;
}
impl GetStdout for Command {
fn get_stdout(&mut self) -> Result<Vec<u8>> {
let Output { stdout, status, .. } = self.stderr(Stdio::inherit()).output()?;
if !status.success() {
bail!("command exited with exit code {}", status);
}
Ok(stdout)
}
}
pub fn flake_prefetch(flake_ref: String) -> Result<String> {
#[derive(Deserialize)]
struct PrefetchOutput {
hash: String,
}
eprintln!("$ nix flake prefetch --json {flake_ref}");
Ok(serde_json::from_slice::<PrefetchOutput>(
&Command::new("nix")
.arg("flake")
.arg("prefetch")
.arg("--json")
.arg(flake_ref)
.get_stdout()?,
)?
.hash)
}

View File

@ -1,12 +1,13 @@
use crate::fetcher::SimpleFlakeFetcher;
use crate::{fetcher::SimpleFlakeFetcher, impl_fetcher};
pub struct FetchFromSourcehut<'a>(pub Option<&'a str>);
pub struct FetchFromSourcehut(pub Option<String>);
impl_fetcher!(FetchFromSourcehut);
impl<'a> SimpleFlakeFetcher<'a> for FetchFromSourcehut<'a> {
impl<'a> SimpleFlakeFetcher<'a> for FetchFromSourcehut {
const FLAKE_TYPE: &'static str = "sourcehut";
const NAME: &'static str = "fetchFromSourcehut";
fn host(&self) -> Option<&'a str> {
self.0
fn host(&'a self) -> &'a Option<String> {
&self.0
}
}

View File

@ -6,10 +6,10 @@ use clap::Parser;
use url::Host;
use crate::{
cli::{Fetcher, Opts},
cli::{FetcherFunction, Opts},
fetcher::{
FetchFromGitHub, FetchFromGitLab, FetchFromSourcehut, Fetchgit, Fetchhg,
SimpleFlakeFetcher, UrlFlakeFetcher,
FetchFromGitHub, FetchFromGitLab, FetchFromSourcehut, Fetcher, FetcherDispatch, Fetchgit,
Fetchhg,
},
};
@ -18,54 +18,50 @@ use std::io::stdout;
fn main() -> Result<()> {
let opts = Opts::parse();
let out = &mut stdout().lock();
let indent = &" ".repeat(opts.indent);
match (opts.fetcher, opts.url.host()) {
(Some(Fetcher::FetchFromGitHub), Some(host)) => {
FetchFromGitHub((host != Host::Domain("github.com")).then_some(&host.to_string()))
.fetch_nix(out, &opts.url, opts.rev, indent)?;
let fetcher: FetcherDispatch = match (opts.fetcher, opts.url.host()) {
(None | Some(FetcherFunction::FetchFromGitHub), Some(Host::Domain("github.com"))) => {
FetchFromGitHub(None).into()
}
(None, Some(Host::Domain("github.com"))) => {
FetchFromGitHub(None).fetch_nix(out, &opts.url, opts.rev, indent)?;
(Some(FetcherFunction::FetchFromGitHub), Some(host)) => {
FetchFromGitHub(Some(host.to_string())).into()
}
(Some(Fetcher::FetchFromGitLab), Some(host)) => {
FetchFromGitLab((host != Host::Domain("github.com")).then_some(&host.to_string()))
.fetch_nix(out, &opts.url, opts.rev, indent)?;
(None | Some(FetcherFunction::FetchFromGitLab), Some(Host::Domain("gitlab.com"))) => {
FetchFromGitLab(None).into()
}
(None, Some(Host::Domain(host))) if host.starts_with("gitlab.") => {
FetchFromGitLab((host != "gitlab.com").then_some(host))
.fetch_nix(out, &opts.url, opts.rev, indent)?;
FetchFromGitLab(Some(host.into())).into()
}
(Some(FetcherFunction::FetchFromGitLab), Some(host)) => {
FetchFromGitLab(Some(host.to_string())).into()
}
(Some(Fetcher::FetchFromSourcehut), Some(host)) => {
FetchFromSourcehut((host != Host::Domain("git.sr.ht")).then_some(&host.to_string()))
.fetch_nix(out, &opts.url, opts.rev, indent)?;
(None | Some(FetcherFunction::FetchFromSourcehut), Some(Host::Domain("git.sr.ht"))) => {
FetchFromSourcehut(None).into()
}
(None, Some(Host::Domain("git.sr.ht"))) => {
FetchFromSourcehut(None).fetch_nix(out, &opts.url, opts.rev, indent)?;
(Some(FetcherFunction::FetchFromSourcehut), Some(host)) => {
FetchFromSourcehut(Some(host.to_string())).into()
}
(
Some(
fetcher @ (Fetcher::FetchFromGitHub
| Fetcher::FetchFromGitLab
| Fetcher::FetchFromSourcehut),
fetcher @ (FetcherFunction::FetchFromGitHub
| FetcherFunction::FetchFromGitLab
| FetcherFunction::FetchFromSourcehut),
),
None,
) => {
bail!("{fetcher:?} does not support URLs without a host");
}
(Some(Fetcher::Fetchgit), _) | (None, _) => {
Fetchgit.fetch_nix(out, &opts.url, opts.rev, indent)?;
}
(Some(FetcherFunction::Fetchgit), _) | (None, _) => Fetchgit.into(),
(Some(Fetcher::Fetchhg), _) => {
Fetchhg.fetch_nix(out, &opts.url, opts.rev, indent)?;
}
}
(Some(FetcherFunction::Fetchhg), _) => Fetchhg.into(),
};
fetcher.fetch_nix(&mut stdout().lock(), &opts.url, opts.rev, indent)?;
Ok(())
}