use crate::{
hakari::{DepFormatVersion, WorkspaceHackLineStyle},
HakariBuilder, HakariOutputOptions, TomlOutError, UnifyTargetHost,
};
use guppy::{
errors::TargetSpecError,
graph::{cargo::CargoResolverVersion, summaries::PackageSetSummary, PackageGraph},
};
use serde::{Deserialize, Serialize};
use std::{collections::BTreeMap, fmt, str::FromStr};
use toml::Serializer;
pub static DEFAULT_CONFIG_PATH: &str = ".config/hakari.toml";
pub static FALLBACK_CONFIG_PATH: &str = ".guppy/hakari.toml";
#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct HakariConfig {
#[serde(flatten)]
pub builder: HakariBuilderSummary,
#[serde(flatten)]
pub output: OutputOptionsSummary,
}
impl FromStr for HakariConfig {
type Err = toml::de::Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
toml::from_str(input)
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct HakariBuilderSummary {
pub hakari_package: Option<String>,
#[serde(alias = "version")]
pub resolver: CargoResolverVersion,
#[serde(default)]
pub unify_target_host: UnifyTargetHost,
#[serde(default)]
pub output_single_feature: bool,
#[serde(default)]
pub dep_format_version: DepFormatVersion,
#[serde(default)]
pub workspace_hack_line_style: WorkspaceHackLineStyle,
#[serde(default)]
pub platforms: Vec<String>,
#[serde(default)]
pub traversal_excludes: PackageSetSummary,
#[serde(default)]
pub final_excludes: PackageSetSummary,
#[serde(
default,
skip_serializing_if = "BTreeMap::is_empty",
with = "registries_impl"
)]
pub registries: BTreeMap<String, String>,
}
impl HakariBuilderSummary {
pub fn new(builder: &HakariBuilder<'_>) -> Result<Self, TargetSpecError> {
Ok(Self {
hakari_package: builder
.hakari_package()
.map(|package| package.name().to_string()),
platforms: builder
.platforms()
.map(|triple_str| triple_str.to_owned())
.collect::<Vec<_>>(),
resolver: builder.resolver(),
traversal_excludes: PackageSetSummary::from_package_ids(
builder.graph(),
builder.traversal_excludes_only(),
)
.expect("all package IDs are valid"),
final_excludes: PackageSetSummary::from_package_ids(
builder.graph(),
builder.final_excludes(),
)
.expect("all package IDs are valid"),
registries: builder
.registries
.iter()
.map(|(name, url)| (name.clone(), url.clone()))
.collect(),
unify_target_host: builder.unify_target_host(),
output_single_feature: builder.output_single_feature(),
dep_format_version: builder.dep_format_version,
workspace_hack_line_style: builder.workspace_hack_line_style,
})
}
pub fn to_hakari_builder<'g>(
&self,
graph: &'g PackageGraph,
) -> Result<HakariBuilder<'g>, guppy::Error> {
HakariBuilder::from_summary(graph, self)
}
pub fn to_string(&self) -> Result<String, toml::ser::Error> {
let mut dst = String::new();
self.write_to_string(&mut dst)?;
Ok(dst)
}
pub fn write_comment(&self, mut out: impl fmt::Write) -> Result<(), TomlOutError> {
let summary = self.to_string().map_err(|err| TomlOutError::Toml {
context: "while serializing HakariBuilderSummary as comment".into(),
err,
})?;
for line in summary.lines() {
if line.is_empty() {
writeln!(out, "#")?;
} else {
writeln!(out, "# {}", line)?;
}
}
Ok(())
}
pub fn write_to_string(&self, dst: &mut String) -> Result<(), toml::ser::Error> {
let mut serializer = Serializer::pretty(dst);
serializer.pretty_array(false);
self.serialize(&mut serializer)
}
}
impl<'g> HakariBuilder<'g> {
pub fn to_summary(&self) -> Result<HakariBuilderSummary, TargetSpecError> {
HakariBuilderSummary::new(self)
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub struct OutputOptionsSummary {
#[serde(default)]
exact_versions: bool,
#[serde(default)]
absolute_paths: bool,
#[serde(default)]
builder_summary: bool,
}
impl OutputOptionsSummary {
pub fn new(options: &HakariOutputOptions) -> Self {
Self {
exact_versions: options.exact_versions,
absolute_paths: options.absolute_paths,
builder_summary: options.builder_summary,
}
}
pub fn to_options(&self) -> HakariOutputOptions {
HakariOutputOptions {
exact_versions: self.exact_versions,
absolute_paths: self.absolute_paths,
builder_summary: self.builder_summary,
}
}
}
mod registries_impl {
use super::*;
use serde::{Deserializer, Serializer};
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct RegistryDe {
index: String,
}
#[derive(Debug, Serialize)]
struct RegistrySer<'a> {
index: &'a str,
}
pub fn serialize<S>(
registry_map: &BTreeMap<String, String>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let ser_map: BTreeMap<_, _> = registry_map
.iter()
.map(|(name, index)| {
(
name.as_str(),
RegistrySer {
index: index.as_str(),
},
)
})
.collect();
ser_map.serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<BTreeMap<String, String>, D::Error>
where
D: Deserializer<'de>,
{
let de_map = BTreeMap::<String, RegistryDe>::deserialize(deserializer)?;
let registry_map = de_map
.into_iter()
.map(|(name, RegistryDe { index })| (name, index))
.collect();
Ok(registry_map)
}
}
#[cfg(test)]
mod tests {
use super::*;
use fixtures::json::*;
#[test]
fn parse_registries() {
static PARSE_REGISTRIES_INPUT: &str = r#"
resolver = "2"
[traversal-excludes]
third-party = [
{ name = "serde_derive", registry = "my-registry" },
]
[registries]
my-registry = { index = "https://github.com/fakeorg/crates.io-index" }
your-registry = { index = "https://foobar" }
"#;
let summary: HakariBuilderSummary =
toml::from_str(PARSE_REGISTRIES_INPUT).expect("failed to parse toml");
let builder = summary
.to_hakari_builder(JsonFixture::metadata_alternate_registries().graph())
.expect("summary => builder conversion");
assert_eq!(
summary.registries.get("my-registry").map(|s| s.as_str()),
Some(METADATA_ALTERNATE_REGISTRY_URL),
"my-registry is correct"
);
assert_eq!(
summary.registries.get("your-registry").map(|s| s.as_str()),
Some("https://foobar"),
"your-registry is correct"
);
let summary2 = builder.to_summary().expect("builder => summary conversion");
let builder2 = summary
.to_hakari_builder(JsonFixture::metadata_alternate_registries().graph())
.expect("summary2 => builder2 conversion");
assert_eq!(
builder.traversal_excludes, builder2.traversal_excludes,
"builder == builder2 traversal excludes"
);
let serialized = toml::to_string(&summary2).expect("serialized to TOML correctly");
let summary3: HakariBuilderSummary =
toml::from_str(&serialized).expect("deserialized from TOML correctly");
assert_eq!(
summary2, summary3,
"summary => serialized => summary roundtrip"
);
}
}