Skip to content

Commit

Permalink
Remove empty maps via custom serialization
Browse files Browse the repository at this point in the history
  • Loading branch information
lucperkins committed Aug 21, 2024
1 parent ae745d8 commit f4e173e
Show file tree
Hide file tree
Showing 3 changed files with 229 additions and 5 deletions.
14 changes: 12 additions & 2 deletions src/cli/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,9 @@ impl FlakeHubClient {
return Err(e).wrap_err(err_text)?;
};

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

Ok(res)
Ok(paths)
}

async fn project_and_url(
Expand Down Expand Up @@ -519,6 +519,16 @@ fn parse_release_ref(release_ref: &str) -> Result<ReleaseRef, FhError> {
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
25 changes: 22 additions & 3 deletions src/cli/cmd/paths.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use std::{collections::HashMap, process::ExitCode};

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

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

/// TODO
/// Display all output paths that are derivations in the specified flake release.
#[derive(Debug, Parser)]
pub(crate) struct PathsSubcommand {
/// TODO
Expand Down Expand Up @@ -34,9 +34,28 @@ impl CommandExecute for PathsSubcommand {
}
}

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

// The custom serializer converts empty maps into nulls
impl Serialize for PathNode {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
PathNode::Path(s) => s.serialize(serializer),
PathNode::PathMap(map) => {
if map.is_empty() {
serializer.serialize_none()
} else {
map.serialize(serializer)
}
}
}
}
}
195 changes: 195 additions & 0 deletions src/cli/refs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
use std::fmt::Display;

use serde::{Deserialize, Serialize};

use crate::{cli::cmd::list::FLAKEHUB_WEB_ROOT, flakehub_url};

use super::error::FhError;

// Parses a flake reference as a string to construct paths of the form:
// https://api.flakehub.com/f/{org}/{flake}/{version_constraint}/output/{attr_path}
pub(crate) struct FlakeOutputRef {
pub(crate) org: String,
pub(crate) project: String,
pub(crate) version_constraint: String,
pub(crate) attr_path: String,
}

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

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

fn try_from(output_ref: String) -> Result<Self, Self::Error> {
let parts: Vec<&str> = output_ref.split('#').collect();

if let Some(release_parts) = parts.first() {
let Some(attr_path) = parts.get(1) else {
Err(FhError::MalformedFlakeOutputRef(
output_ref,
String::from("missing the output attribute path"),
))?
};

match release_parts.split('/').collect::<Vec<_>>()[..] {
[org, project, version_constraint] => {
validate_segment(org, "org")?;
validate_segment(project, "project")?;
validate_segment(version_constraint, "version constraint")?;
validate_segment(attr_path, "attribute path")?;

Ok(FlakeOutputRef {
org: org.to_string(),
project: project.to_string(),
version_constraint: version_constraint.to_string(),
attr_path: attr_path.to_string(),
})
}
_ => Err(FhError::MalformedFlakeOutputRef(
output_ref,
String::from(
"release reference must be of the form {{org}}/{{project}}/{{version_req}}",
),
)),
}
} else {
Err(FhError::MalformedFlakeOutputRef(
output_ref,
String::from(
"must be of the form {{org}}/{{project}}/{{version_req}}#{{attr_path}}",
),
))
}
}
}

pub(crate) fn parse_flake_output_ref_with_default_path(
frontend_addr: &url::Url,
output_ref: &str,
default_path: &str,
) -> Result<FlakeOutputRef, FhError> {
let with_default_output_path = match output_ref.split('#').collect::<Vec<_>>()[..] {
[_release, _output_path] => output_ref.to_string(),
[_release] => format!("{}#{}", output_ref, default_path),
_ => {
return Err(FhError::MalformedFlakeOutputRef(
output_ref.to_string(),
String::from(
"must be of the form {{org}}/{{project}}/{{version_req}}#{{attr_path}}",
),
))
}
};

parse_flake_output_ref(frontend_addr, &with_default_output_path)
}

pub(crate) fn parse_flake_output_ref(
frontend_addr: &url::Url,
output_ref: &str,
) -> Result<FlakeOutputRef, FhError> {
// Ensures that users can use both forms:
// 1. https://flakehub/f/{org}/{project}/{version_req}#{output}
// 2. {org}/{project}/{version_req}#{output}
let output_ref = String::from(
output_ref
.strip_prefix(frontend_addr.join("f/")?.as_str())
.unwrap_or(output_ref),
);

output_ref.try_into()
}

// Simple flake refs are of the form {org}/{project}, for example NixOS/nixpkgs
#[derive(Clone, Deserialize, Serialize)]
pub(crate) struct SimpleFlakeRef {
pub(crate) org: String,
pub(crate) project: String,
}

impl SimpleFlakeRef {
pub(crate) fn url(&self) -> url::Url {
flakehub_url!(FLAKEHUB_WEB_ROOT, "flake", &self.org, &self.project)
}
}

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

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

fn try_from(flake_ref: String) -> Result<Self, Self::Error> {
let (org, project) = match flake_ref.split('/').collect::<Vec<_>>()[..] {
// `nixos/nixpkgs`
[org, repo] => (org, repo),
_ => {
return Err(FhError::Parse(format!(
"flake ref {flake_ref} invalid; must be of the form {{org}}/{{project}}"
)))
}
};
Ok(Self {
org: String::from(org),
project: String::from(project),
})
}
}

#[derive(Deserialize, Serialize)]
pub(crate) struct VersionRef {
pub(crate) version: semver::Version,
pub(crate) simplified_version: semver::Version,
}

#[derive(Deserialize, Serialize)]
pub(crate) struct OrgRef {
pub(crate) name: String,
}

#[derive(Deserialize, Serialize)]
pub(crate) struct ReleaseRef {
pub(crate) version: String,
}

/*
// 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<_>>()[..] {
[org, project, version_req] => {
validate_segment(org)?;
validate_segment(project)?;
validate_segment(version_req)?;
Ok(flake_ref.to_string())
}
_ => Err(FhError::FlakeParse(format!(
"flake ref {flake_ref} invalid; must be of the form {{org}}/{{project}}/{{version_req}}"
))),
}
}
*/

// Ensure that orgs, project names, and the like don't contain whitespace.
// This function may apply other validations in the future.
fn validate_segment(s: &str, field: &str) -> Result<(), FhError> {
if s.chars().any(char::is_whitespace) {
return Err(FhError::Parse(format!(
"{} in path segment contains whitespace: \"{}\"",
field, s
)));
}

Ok(())
}

0 comments on commit f4e173e

Please sign in to comment.