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