Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add paths command for listing all output paths for a release #138

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/cli/cmd/apply/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ fn parse_output_ref(
let output_ref =
super::parse_flake_output_ref(frontend_addr, &with_default_output_path)?.to_string();

let parsed = super::parse_release_ref(&output_ref)?;
let parsed = super::validate_release_ref(&output_ref)?;

parsed.try_into()
}
Expand Down
88 changes: 83 additions & 5 deletions src/cli/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ pub(crate) mod eject;
pub(crate) mod init;
pub(crate) mod list;
pub(crate) mod login;
pub(crate) mod paths;
pub(crate) mod resolve;
pub(crate) mod search;
pub(crate) mod status;

use std::{fmt::Display, process::Stdio};
use std::{collections::HashMap, fmt::Display, process::Stdio};

use color_eyre::eyre::WrapErr;
use once_cell::sync::Lazy;
use paths::PathNode;
use reqwest::{
header::{HeaderMap, HeaderValue, ACCEPT, AUTHORIZATION},
Client,
Expand Down Expand Up @@ -72,6 +74,7 @@ pub(crate) enum FhSubcommands {
Init(init::InitSubcommand),
List(list::ListSubcommand),
Login(login::LoginSubcommand),
Paths(paths::PathsSubcommand),
Resolve(resolve::ResolveSubcommand),
Search(search::SearchSubcommand),
Status(status::StatusSubcommand),
Expand Down Expand Up @@ -178,6 +181,36 @@ impl FlakeHubClient {
get(url, true).await
}

async fn paths(
api_addr: &str,
release_ref: &ReleaseRef,
) -> Result<HashMap<String, PathNode>, FhError> {
let ReleaseRef {
org,
project,
version_constraint,
} = release_ref;

let url = flakehub_url!(api_addr, "f", org, project, version_constraint, "outputs");
tracing::debug!(
url = url.to_string(),
r#ref = release_ref.to_string(),
"Fetching all output paths for flake release"
);
let client = make_base_client(true).await?;
let res = client.get(url.to_string()).send().await?;

// Enrich the CLI error text with the error returned by FlakeHub
if let Err(e) = res.error_for_status_ref() {
let err_text = res.text().await?;
return Err(e).wrap_err(err_text)?;
};

let paths = res.json::<HashMap<String, PathNode>>().await?;

Ok(paths)
}

async fn project_and_url(
api_addr: &str,
org: &str,
Expand Down Expand Up @@ -277,6 +310,22 @@ impl Display for FlakeOutputRef {
}
}

struct ReleaseRef {
org: String,
project: String,
version_constraint: String,
}

impl Display for ReleaseRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}/{}/{}",
self.org, self.project, self.version_constraint
)
}
}

impl TryFrom<String> for FlakeOutputRef {
type Error = FhError;

Expand Down Expand Up @@ -442,17 +491,46 @@ fn parse_flake_output_ref(
}

// Ensure that release refs are of the form {org}/{project}/{version_req}
fn parse_release_ref(flake_ref: &str) -> Result<String, FhError> {
match flake_ref.split('/').collect::<Vec<_>>()[..] {
fn validate_release_ref(release_ref: &str) -> Result<String, FhError> {
match release_ref.split('/').collect::<Vec<_>>()[..] {
[org, project, version_req] => {
validate_segment(org)?;
validate_segment(project)?;
validate_segment(version_req)?;

Ok(flake_ref.to_string())
Ok(release_ref.to_string())
}
_ => Err(FhError::FlakeParse(format!(
"flake ref {flake_ref} invalid; must be of the form {{org}}/{{project}}/{{version_req}}"
"release ref {release_ref} invalid; must be of the form {{org}}/{{project}}/{{version_req}}"
))),
}
}

fn parse_release_ref(release_ref: &str) -> Result<ReleaseRef, FhError> {
match release_ref.split('/').collect::<Vec<_>>()[..] {
[org, project, version_constraint] => {
validate_segment(org)?;
validate_segment(project)?;
validate_segment(version_constraint)?;

Ok(ReleaseRef {
org: org.to_string(),
project: project.to_string(),
version_constraint: version_constraint.to_string(),
})
}
[org, project] => {
validate_segment(org)?;
validate_segment(project)?;

Ok(ReleaseRef {
org: org.to_string(),
project: project.to_string(),
version_constraint: "*".to_string(),
})
}
_ => Err(FhError::ReleaseRefParse(format!(
"release ref {release_ref} invalid; must be of the form {{org}}/{{project}}/{{version_req}}"
))),
}
}
Expand Down
68 changes: 68 additions & 0 deletions src/cli/cmd/paths.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use std::{collections::HashMap, process::ExitCode};

use clap::Parser;
use serde::{Deserialize, Serialize};

use super::{parse_release_ref, print_json, CommandExecute, FlakeHubClient};

/// Display all output paths that are derivations in the specified flake release.
#[derive(Debug, Parser)]
pub(crate) struct PathsSubcommand {
/// TODO
release_ref: String,

#[clap(from_global)]
api_addr: url::Url,
}

#[async_trait::async_trait]
impl CommandExecute for PathsSubcommand {
#[tracing::instrument(skip_all)]
async fn execute(self) -> color_eyre::Result<ExitCode> {
let release_ref = parse_release_ref(&self.release_ref)?;

let mut paths = FlakeHubClient::paths(self.api_addr.as_ref(), &release_ref).await?;
clear_nulls(&mut paths);

tracing::debug!(
r#ref = release_ref.to_string(),
"Successfully fetched output paths for release"
);

if paths.is_empty() {
tracing::warn!("Flake release provides no output paths");
}

print_json(paths)?;
Ok(ExitCode::SUCCESS)
}
}

#[derive(Deserialize, Serialize)]
#[serde(untagged)]
pub(crate) enum PathNode {
Path(String),
PathMap(HashMap<String, PathNode>),
}

// Recursively removes any nulls from the output path tree
fn clear_nulls(map: &mut HashMap<String, PathNode>) {
let keys_to_remove: Vec<String> = map
.iter_mut()
.filter_map(|(key, value)| match value {
PathNode::PathMap(ref mut inner_map) => {
clear_nulls(inner_map);
if inner_map.is_empty() {
Some(key.clone())
} else {
None
}
}
_ => None,
})
.collect();

for key in keys_to_remove {
map.remove(&key);
}
}
3 changes: 3 additions & 0 deletions src/cli/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ pub(crate) enum FhError {
#[error("the flake has no inputs")]
NoInputs,

#[error("release ref parse error: {0}")]
ReleaseRefParse(String),

#[error("template error: {0}")]
Render(#[from] handlebars::RenderError),

Expand Down
Loading
Loading