1use crate::{
9 HakariBuilder, HakariOutputOptions, TomlOutError, UnifyTargetHost,
10 hakari::{DepFormatVersion, WorkspaceHackLineStyle},
11};
12use guppy::{
13 errors::TargetSpecError,
14 graph::{PackageGraph, cargo::CargoResolverVersion, summaries::PackageSetSummary},
15};
16use serde::{Deserialize, Serialize};
17use std::{collections::BTreeMap, fmt, str::FromStr};
18
19pub static DEFAULT_CONFIG_PATH: &str = ".config/hakari.toml";
21
22pub static FALLBACK_CONFIG_PATH: &str = ".guppy/hakari.toml";
24
25#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
29#[serde(rename_all = "kebab-case")]
30#[non_exhaustive]
31pub struct HakariConfig {
32 #[serde(flatten)]
34 pub builder: HakariBuilderSummary,
35
36 #[serde(flatten)]
38 pub output: OutputOptionsSummary,
39}
40
41impl FromStr for HakariConfig {
42 type Err = toml::de::Error;
43
44 fn from_str(input: &str) -> Result<Self, Self::Err> {
46 toml::from_str(input)
47 }
48}
49
50#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
57#[serde(rename_all = "kebab-case")]
58#[non_exhaustive]
59pub struct HakariBuilderSummary {
60 pub hakari_package: Option<String>,
62
63 #[serde(alias = "version")]
67 pub resolver: CargoResolverVersion,
68
69 #[serde(default)]
71 pub unify_target_host: UnifyTargetHost,
72
73 #[serde(default)]
75 pub output_single_feature: bool,
76
77 #[serde(default)]
79 pub dep_format_version: DepFormatVersion,
80
81 #[serde(default)]
83 pub workspace_hack_line_style: WorkspaceHackLineStyle,
84
85 #[serde(default)]
87 pub platforms: Vec<String>,
88
89 #[serde(default)]
91 pub traversal_excludes: PackageSetSummary,
92
93 #[serde(default)]
95 pub final_excludes: PackageSetSummary,
96
97 #[serde(
102 default,
103 skip_serializing_if = "BTreeMap::is_empty",
104 with = "registries_impl"
105 )]
106 pub registries: BTreeMap<String, String>,
107}
108
109impl HakariBuilderSummary {
110 pub fn new(builder: &HakariBuilder<'_>) -> Result<Self, TargetSpecError> {
117 Ok(Self {
118 hakari_package: builder
119 .hakari_package()
120 .map(|package| package.name().to_string()),
121 platforms: builder
122 .platforms()
123 .map(|triple_str| triple_str.to_owned())
124 .collect::<Vec<_>>(),
125 resolver: builder.resolver(),
126 traversal_excludes: PackageSetSummary::from_package_ids(
127 builder.graph(),
128 builder.traversal_excludes_only(),
129 )
130 .expect("all package IDs are valid"),
131 final_excludes: PackageSetSummary::from_package_ids(
132 builder.graph(),
133 builder.final_excludes(),
134 )
135 .expect("all package IDs are valid"),
136 registries: builder
137 .registries
138 .iter()
139 .map(|registry| (registry.name.clone(), registry.url.clone()))
140 .collect(),
141 unify_target_host: builder.unify_target_host(),
142 output_single_feature: builder.output_single_feature(),
143 dep_format_version: builder.dep_format_version,
144 workspace_hack_line_style: builder.workspace_hack_line_style,
145 })
146 }
147
148 pub fn to_hakari_builder<'g>(
153 &self,
154 graph: &'g PackageGraph,
155 ) -> Result<HakariBuilder<'g>, guppy::Error> {
156 HakariBuilder::from_summary(graph, self)
157 }
158
159 pub fn to_string(&self) -> Result<String, toml::ser::Error> {
163 let mut dst = String::new();
164 self.write_to_string(&mut dst)?;
165 Ok(dst)
166 }
167
168 pub fn write_comment(&self, mut out: impl fmt::Write) -> Result<(), TomlOutError> {
173 let summary = self.to_string().map_err(|err| TomlOutError::Toml {
175 context: "while serializing HakariBuilderSummary as comment".into(),
176 err,
177 })?;
178 for line in summary.lines() {
179 if line.is_empty() {
180 writeln!(out, "#")?;
181 } else {
182 writeln!(out, "# {line}")?;
183 }
184 }
185 Ok(())
186 }
187
188 pub fn write_to_string(&self, dst: &mut String) -> Result<(), toml::ser::Error> {
192 let table = toml::Table::try_from(self)?;
193 guppy::graph::summaries::toml_compat::write_table(&table, dst)
194 }
195}
196
197impl HakariBuilder<'_> {
198 pub fn to_summary(&self) -> Result<HakariBuilderSummary, TargetSpecError> {
205 HakariBuilderSummary::new(self)
206 }
207}
208
209#[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]
213#[serde(rename_all = "kebab-case")]
214#[non_exhaustive]
215pub struct OutputOptionsSummary {
216 #[serde(default)]
218 exact_versions: bool,
219
220 #[serde(default)]
222 absolute_paths: bool,
223
224 #[serde(default)]
226 builder_summary: bool,
227}
228
229impl OutputOptionsSummary {
230 pub fn new(options: &HakariOutputOptions) -> Self {
232 Self {
233 exact_versions: options.exact_versions,
234 absolute_paths: options.absolute_paths,
235 builder_summary: options.builder_summary,
236 }
237 }
238
239 pub fn to_options(&self) -> HakariOutputOptions {
241 HakariOutputOptions {
242 exact_versions: self.exact_versions,
243 absolute_paths: self.absolute_paths,
244 builder_summary: self.builder_summary,
245 }
246 }
247}
248
249mod registries_impl {
250 use super::*;
251 use serde::{Deserializer, Serializer};
252
253 #[derive(Debug, Deserialize)]
254 #[serde(deny_unknown_fields)]
255 struct RegistryDe {
256 index: String,
257 }
258
259 #[derive(Debug, Serialize)]
260 struct RegistrySer<'a> {
261 index: &'a str,
262 }
263
264 pub fn serialize<S>(
266 registry_map: &BTreeMap<String, String>,
267 serializer: S,
268 ) -> Result<S::Ok, S::Error>
269 where
270 S: Serializer,
271 {
272 let ser_map: BTreeMap<_, _> = registry_map
273 .iter()
274 .map(|(name, index)| {
275 (
276 name.as_str(),
277 RegistrySer {
278 index: index.as_str(),
279 },
280 )
281 })
282 .collect();
283 ser_map.serialize(serializer)
284 }
285
286 pub fn deserialize<'de, D>(deserializer: D) -> Result<BTreeMap<String, String>, D::Error>
288 where
289 D: Deserializer<'de>,
290 {
291 let de_map = BTreeMap::<String, RegistryDe>::deserialize(deserializer)?;
292 let registry_map = de_map
293 .into_iter()
294 .map(|(name, RegistryDe { index })| (name, index))
295 .collect();
296 Ok(registry_map)
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use fixtures::json::*;
304
305 #[test]
306 fn parse_registries() {
307 static PARSE_REGISTRIES_INPUT: &str = r#"
308 resolver = "2"
309
310 [traversal-excludes]
311 third-party = [
312 { name = "serde_derive", registry = "my-registry" },
313 ]
314
315 [registries]
316 my-registry = { index = "https://github.com/fakeorg/crates.io-index" }
317 your-registry = { index = "https://foobar" }
318 "#;
319
320 let summary: HakariBuilderSummary =
321 toml::from_str(PARSE_REGISTRIES_INPUT).expect("failed to parse toml");
322 let builder = summary
324 .to_hakari_builder(JsonFixture::metadata_alternate_registries().graph())
325 .expect("summary => builder conversion");
326
327 assert_eq!(
328 summary.registries.get("my-registry").map(|s| s.as_str()),
329 Some(METADATA_ALTERNATE_REGISTRY_URL),
330 "my-registry is correct"
331 );
332 assert_eq!(
333 summary.registries.get("your-registry").map(|s| s.as_str()),
334 Some("https://foobar"),
335 "your-registry is correct"
336 );
337
338 let summary2 = builder.to_summary().expect("builder => summary conversion");
339 let builder2 = summary
340 .to_hakari_builder(JsonFixture::metadata_alternate_registries().graph())
341 .expect("summary2 => builder2 conversion");
342 assert_eq!(
343 builder.traversal_excludes, builder2.traversal_excludes,
344 "builder == builder2 traversal excludes"
345 );
346
347 let serialized = toml::to_string(&summary2).expect("serialized to TOML correctly");
348 let summary3: HakariBuilderSummary =
349 toml::from_str(&serialized).expect("deserialized from TOML correctly");
350 assert_eq!(
351 summary2, summary3,
352 "summary => serialized => summary roundtrip"
353 );
354 }
355}