1#[macro_use]
2mod macros;
3mod toml_helpers;
4use toml_helpers::*;
5mod package;
6pub use package::*;
7mod profile_settings;
8pub use profile_settings::*;
9mod workspace;
10pub use workspace::*;
11
12use merge_it::*;
13#[cfg(feature = "schemars")]
14use schemars::JsonSchema;
15use serde::{Deserialize, Serialize};
16use serde_json::Value;
17use std::collections::{BTreeMap, BTreeSet};
18use std::mem;
19use std::path::PathBuf;
20use toml_edit::{Array, ArrayOfTables, DocumentMut, InlineTable, Item, Table, Value as TomlValue};
21
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Merge, Default)]
28#[cfg_attr(feature = "schemars", derive(JsonSchema))]
29#[serde(rename_all = "kebab-case")]
30pub struct Manifest {
31 #[merge(with = merge_options)]
33 pub workspace: Option<Workspace>,
34
35 #[merge(with = merge_options)]
37 pub package: Option<Package>,
38
39 #[merge(with = merge_options)]
41 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub lib: Option<Product>,
43
44 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
46 pub bin: BTreeSet<Product>,
47
48 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
50 pub target: BTreeMap<String, Target>,
51
52 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
54 pub patch: BTreeMap<String, BTreeMap<String, Dependency>>,
55
56 #[serde(default, skip_serializing_if = "Option::is_none")]
58 #[merge(with = merge_options)]
59 pub profile: Option<Profiles>,
60
61 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
63 pub bench: BTreeSet<Product>,
64
65 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
67 pub test: BTreeSet<Product>,
68
69 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
71 pub example: BTreeSet<Product>,
72
73 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub lints: Option<Inheritable<Lints>>,
76
77 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
79 pub features: BTreeMap<String, BTreeSet<String>>,
80
81 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
83 #[merge(with = merge_btree_maps)]
84 pub dependencies: BTreeMap<String, Dependency>,
85
86 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
88 #[merge(with = merge_btree_maps)]
89 pub dev_dependencies: BTreeMap<String, Dependency>,
90
91 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
93 #[merge(with = merge_btree_maps)]
94 pub build_dependencies: BTreeMap<String, Dependency>,
95}
96
97impl Manifest {
98 pub fn as_document(&self) -> DocumentMut {
100 let mut document = DocumentMut::new();
101
102 if let Some(workspace) = &self.workspace {
103 document.insert("workspace", workspace.as_toml_value());
104 }
105
106 add_value!(self, document => package, lib, profile);
107
108 if !self.target.is_empty() {
109 let mut table = Table::from_iter(
110 self.target
111 .iter()
112 .map(|(name, target)| (toml_edit::Key::from(name), target.as_toml_value())),
113 );
114
115 table.set_implicit(true);
116
117 document["target"] = table.into();
118 }
119
120 if !self.bin.is_empty() {
121 let array =
122 ArrayOfTables::from_iter(self.bin.iter().map(|i| match i.as_toml_value() {
123 Item::Table(table) => table,
124 _ => panic!("Found non-tables for cargo toml bin"),
125 }));
126
127 document["bin"] = array.into();
128 }
129
130 if !self.bench.is_empty() {
131 let array = ArrayOfTables::from_iter(self.bench.iter().map(
132 |i| match i.as_toml_value() {
133 Item::Table(table) => table,
134 _ => panic!("Found non-tables for cargo toml bench"),
135 },
136 ));
137
138 document["bench"] = array.into();
139 }
140
141 if !self.test.is_empty() {
142 let array =
143 ArrayOfTables::from_iter(self.test.iter().map(|i| match i.as_toml_value() {
144 Item::Table(table) => table,
145 _ => panic!("Found non-tables for cargo toml test"),
146 }));
147
148 document["test"] = array.into();
149 }
150
151 if !self.example.is_empty() {
152 let array = ArrayOfTables::from_iter(self.example.iter().map(
153 |i| match i.as_toml_value() {
154 Item::Table(table) => table,
155 _ => panic!("Found non-tables for cargo toml examples"),
156 },
157 ));
158
159 document["example"] = array.into();
160 }
161
162 if let Some(lints) = &self.lints {
163 document["lints"] = match lints {
164 Inheritable::Workspace { workspace } => {
165 Table::from_iter([("workspace", *workspace)]).into()
166 }
167 Inheritable::Value(lints) => lints.as_toml_value(),
168 };
169 }
170
171 add_table!(self, document => dev_dependencies, build_dependencies, dependencies);
172
173 if !self.patch.is_empty() {
174 let mut table = Table::from_iter(self.patch.iter().map(|(name, deps)| {
175 let mut deps_table =
176 Table::from_iter(deps.iter().map(|(dep_name, dep)| {
177 (toml_edit::Key::from(dep_name), dep.as_toml_value())
178 }));
179
180 deps_table.set_implicit(true);
181
182 (toml_edit::Key::from(name), deps_table)
183 }));
184
185 table.set_implicit(true);
186
187 document["patch"] = table.into();
188 }
189
190 if !self.features.is_empty() {
191 document["features"] =
192 Table::from_iter(self.features.iter().map(|(name, features)| {
193 let mut array = Array::from_iter(features);
194 format_array(&mut array);
195
196 (toml_edit::Key::from(name.as_str()), array)
197 }))
198 .into();
199 }
200
201 document
202 }
203}
204
205#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
207#[cfg_attr(feature = "schemars", derive(JsonSchema))]
208#[serde(rename_all = "kebab-case")]
209pub enum LintLevel {
210 Allow,
211 Warn,
212 Deny,
213 Forbid,
214}
215
216impl AsTomlValue for LintLevel {
217 fn as_toml_value(&self) -> Item {
218 let str = match self {
219 Self::Allow => "allow",
220 Self::Warn => "warn",
221 Self::Deny => "deny",
222 Self::Forbid => "forbid",
223 };
224
225 str.into()
226 }
227}
228
229#[track_caller]
230fn item_to_toml_value(item: Item) -> Option<TomlValue> {
231 let output = match item {
232 Item::Value(value) => value,
233 Item::Table(table) => table.into_inline_table().into(),
234 Item::ArrayOfTables(arr) => arr.into_array().into(),
235 _ => return None,
236 };
237
238 Some(output)
239}
240
241#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
243#[cfg_attr(feature = "schemars", derive(JsonSchema))]
244#[serde(deny_unknown_fields)]
245pub struct Lint {
246 pub level: LintLevel,
248
249 pub priority: Option<i8>,
251}
252
253impl AsTomlValue for Lint {
254 fn as_toml_value(&self) -> Item {
255 let mut table = InlineTable::new();
256
257 if let Some(level) = item_to_toml_value(self.level.as_toml_value()) {
258 table.insert("level", level);
259 }
260
261 if let Some(priority) = self.priority {
262 table.insert("priority", i64::from(priority).into());
263 }
264
265 table.into()
266 }
267}
268
269#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
271#[cfg_attr(feature = "schemars", derive(JsonSchema))]
272#[serde(default, rename_all = "kebab-case")]
273#[serde(deny_unknown_fields)]
274pub struct Target {
275 pub dependencies: BTreeMap<String, Dependency>,
277 pub dev_dependencies: BTreeMap<String, Dependency>,
279 pub build_dependencies: BTreeMap<String, Dependency>,
281}
282
283impl AsTomlValue for Target {
284 fn as_toml_value(&self) -> Item {
285 let mut table = Table::new();
286
287 table.set_implicit(true);
288
289 add_table!(self, table => dev_dependencies, dependencies, build_dependencies);
290
291 table.into()
292 }
293}
294
295#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
299#[cfg_attr(feature = "schemars", derive(JsonSchema))]
300#[serde(untagged)]
301pub enum Dependency {
302 Simple(String),
304
305 Inherited(InheritedDependencyDetail), Detailed(Box<DependencyDetail>),
310}
311
312impl AsTomlValue for Dependency {
313 fn as_toml_value(&self) -> Item {
314 match self {
315 Self::Simple(ver) => ver.into(),
316 Self::Inherited(dep) => dep.as_toml_value(),
317 Self::Detailed(dep) => dep.as_toml_value(),
318 }
319 }
320}
321
322impl Dependency {
323 pub fn optional(&self) -> bool {
325 match self {
326 Self::Simple(_) => false,
327 Self::Inherited(dep) => dep.optional,
328 Self::Detailed(dep) => dep.optional,
329 }
330 }
331
332 pub fn features(&self) -> Option<&BTreeSet<String>> {
334 match self {
335 Self::Simple(_) => None,
336 Self::Inherited(dep) => Some(&dep.features),
337 Self::Detailed(dep) => Some(&dep.features),
338 }
339 }
340
341 pub const fn as_simple(&self) -> Option<&String> {
342 if let Self::Simple(v) = self {
343 Some(v)
344 } else {
345 None
346 }
347 }
348
349 pub const fn as_inherited(&self) -> Option<&InheritedDependencyDetail> {
350 if let Self::Inherited(v) = self {
351 Some(v)
352 } else {
353 None
354 }
355 }
356
357 pub const fn as_detailed(&self) -> Option<&DependencyDetail> {
358 if let Self::Detailed(v) = self {
359 Some(v)
360 } else {
361 None
362 }
363 }
364}
365
366impl Merge for Dependency {
367 fn merge(&mut self, other: Self) {
368 match self {
369 Self::Simple(left_simple) => {
370 match other {
371 Self::Simple(right_simple) => *left_simple = right_simple,
372 Self::Inherited(right_inherited) => *self = Self::Inherited(right_inherited),
373 Self::Detailed(mut right_detailed) => {
374 if right_detailed.version.is_none() {
375 let version = mem::take(left_simple);
376
377 right_detailed.version = Some(version);
378 }
379
380 *self = Self::Detailed(right_detailed);
381 }
382 };
383 }
384 Self::Inherited(left_inherited) => match other {
385 Self::Simple(right_simple) => {
388 let features = mem::take(&mut left_inherited.features);
389
390 *self = Self::Detailed(
391 DependencyDetail {
392 version: Some(right_simple),
393 optional: left_inherited.optional,
394 features,
395 ..Default::default()
396 }
397 .into(),
398 );
399 }
400 Self::Inherited(right) => left_inherited.merge(right),
401 Self::Detailed(mut right) => {
402 if left_inherited.optional {
403 right.optional = true;
404 }
405
406 let left_features = mem::take(&mut left_inherited.features);
407
408 right.features.extend(left_features);
409
410 *self = Self::Detailed(right);
411 }
412 },
413 Self::Detailed(left_detailed) => match other {
414 Self::Simple(right_simple) => {
415 if left_detailed.version.is_none() {
416 left_detailed.version = Some(right_simple);
417 }
418 }
419 Self::Inherited(mut right) => {
420 if left_detailed.optional {
421 right.optional = true;
422 }
423
424 let left_features = mem::take(&mut left_detailed.features);
425
426 right.features.extend(left_features);
427
428 *self = Self::Inherited(right);
429 }
430 Self::Detailed(right) => {
431 left_detailed.merge(*right);
432 }
433 },
434 }
435 }
436}
437
438#[allow(clippy::trivially_copy_pass_by_ref)]
440pub(crate) const fn is_false(boolean: &bool) -> bool {
441 !*boolean
442}
443
444#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, Merge)]
446#[cfg_attr(feature = "schemars", derive(JsonSchema))]
447#[serde(rename_all = "kebab-case")]
448#[serde(deny_unknown_fields)]
449pub struct InheritedDependencyDetail {
450 #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
452 pub features: BTreeSet<String>,
453
454 #[serde(default, skip_serializing_if = "crate::is_false")]
456 #[merge(with = overwrite_if_true)]
457 pub optional: bool,
458
459 #[serde(skip_serializing_if = "crate::is_false")]
462 #[merge(with = overwrite_if_true)]
463 pub workspace: bool,
464}
465
466impl AsTomlValue for InheritedDependencyDetail {
467 fn as_toml_value(&self) -> Item {
468 let mut table = InlineTable::new();
469
470 add_bool!(self, table => workspace, optional);
471
472 add_string_list!(self, table => features);
473
474 table.into()
475 }
476}
477
478#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Merge, Default)]
480#[cfg_attr(feature = "schemars", derive(JsonSchema))]
481#[serde(default, rename_all = "kebab-case")]
482#[serde(deny_unknown_fields)]
483pub struct DependencyDetail {
484 #[serde(skip_serializing_if = "Option::is_none")]
486 pub version: Option<String>,
487
488 #[serde(skip_serializing_if = "Option::is_none")]
492 pub package: Option<String>,
493
494 #[serde(skip_serializing_if = "Option::is_none")]
498 pub registry: Option<String>,
499
500 #[serde(skip_serializing_if = "Option::is_none")]
502 pub registry_index: Option<String>,
503
504 #[serde(skip_serializing_if = "Option::is_none")]
506 pub path: Option<String>,
507
508 #[serde(skip_serializing_if = "Option::is_none")]
510 pub git: Option<String>,
511
512 #[serde(skip_serializing_if = "Option::is_none")]
514 pub branch: Option<String>,
515
516 #[serde(skip_serializing_if = "Option::is_none")]
518 pub tag: Option<String>,
519
520 #[serde(skip_serializing_if = "Option::is_none")]
522 pub rev: Option<String>,
523
524 #[serde(skip_serializing_if = "BTreeSet::is_empty")]
528 #[merge(with = BTreeSet::extend)]
529 pub features: BTreeSet<String>,
530
531 #[serde(skip_serializing_if = "crate::is_false")]
537 #[merge(with = overwrite_if_true)]
538 pub optional: bool,
539
540 #[serde(skip_serializing_if = "Option::is_none")]
542 pub default_features: Option<bool>,
543}
544
545impl AsTomlValue for DependencyDetail {
546 fn as_toml_value(&self) -> Item {
547 let mut table = InlineTable::new();
548
549 add_string!(self, table => version, path, package, registry, registry_index, git, branch, tag, rev);
550
551 add_bool!(self, table => optional);
552
553 add_if_false!(self, table => default_features);
554
555 add_string_list!(self, table => features);
556
557 table.into()
558 }
559}
560
561#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
563#[cfg_attr(feature = "schemars", derive(JsonSchema))]
564#[serde(untagged)]
565pub enum Inheritable<T> {
566 #[serde(rename = "workspace")]
568 Workspace {
569 workspace: bool,
570 },
571 Value(T),
572}
573
574impl<T> Inheritable<T> {
575 pub const fn is_workspace(&self) -> bool {
576 matches!(self, Self::Workspace { workspace: true })
577 }
578
579 pub const fn as_value(&self) -> Option<&T> {
580 if let Self::Value(val) = self {
581 Some(val)
582 } else {
583 None
584 }
585 }
586}
587
588impl<T: Merge> Merge for Inheritable<T> {
589 fn merge(&mut self, other: Self) {
590 match self {
591 Self::Workspace { .. } => {
592 *self = other;
593 }
594 Self::Value(content_left) => {
595 match other {
596 Self::Workspace { workspace } => *self = Self::Workspace { workspace },
597 Self::Value(content_right) => content_left.merge(content_right),
598 };
599 }
600 }
601 }
602}
603
604impl<T: AsTomlValue> AsTomlValue for Inheritable<T> {
605 fn as_toml_value(&self) -> Item {
606 match self {
607 Self::Workspace { workspace } => {
608 InlineTable::from_iter([("workspace", *workspace)]).into()
609 }
610 Self::Value(value) => value.as_toml_value(),
611 }
612 }
613}
614
615impl<T: Default> Default for Inheritable<T> {
616 fn default() -> Self {
617 Self::Value(T::default())
618 }
619}
620
621impl<T: Default + PartialEq> Inheritable<T> {
622 pub fn is_default(&self) -> bool {
624 match self {
625 Self::Workspace { .. } => false,
626 Self::Value(v) => T::default() == *v,
627 }
628 }
629}
630
631#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default, Eq, PartialOrd, Ord)]
633#[cfg_attr(feature = "schemars", derive(JsonSchema))]
634pub enum Edition {
635 #[default]
637 #[serde(rename = "2015")]
638 E2015 = 2015,
639 #[serde(rename = "2018")]
641 E2018 = 2018,
642 #[serde(rename = "2021")]
644 E2021 = 2021,
645 #[serde(rename = "2024")]
647 E2024 = 2024,
648}
649
650impl AsTomlValue for Edition {
651 fn as_toml_value(&self) -> Item {
652 let str = match self {
653 Self::E2015 => "2015",
654 Self::E2018 => "2018",
655 Self::E2021 => "2021",
656 Self::E2024 => "2024",
657 };
658
659 str.into()
660 }
661}
662
663#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
665#[cfg_attr(feature = "schemars", derive(JsonSchema))]
666#[serde(
667 untagged,
668 expecting = "the value should be either a boolean or a file path"
669)]
670pub enum OptionalFile {
671 Flag(bool),
673 Path(PathBuf),
675}
676
677impl AsTomlValue for OptionalFile {
678 fn as_toml_value(&self) -> Item {
679 match self {
680 Self::Flag(bool) => (*bool).into(),
681 Self::Path(path) => path.to_string_lossy().as_ref().into(),
682 }
683 }
684}
685
686#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
688#[cfg_attr(feature = "schemars", derive(JsonSchema))]
689#[serde(
690 untagged,
691 expecting = "the value should be either a boolean, or an array of registry names"
692)]
693pub enum Publish {
694 Flag(bool),
695 Registry(BTreeSet<String>),
696}
697
698impl AsTomlValue for Publish {
699 fn as_toml_value(&self) -> Item {
700 match self {
701 Self::Flag(bool) => (*bool).into(),
702 Self::Registry(list) => toml_string_list(list),
703 }
704 }
705}
706
707#[derive(Debug, Default, PartialEq, Eq, Ord, PartialOrd, Copy, Clone, Serialize, Deserialize)]
711#[cfg_attr(feature = "schemars", derive(JsonSchema))]
712#[serde(expecting = "if there's a newer resolver, then this parser has to be updated")]
713#[repr(u8)]
714pub enum Resolver {
715 #[serde(rename = "1")]
716 V1 = 1,
718 #[serde(rename = "2")]
720 V2 = 2,
721 #[serde(rename = "3")]
723 #[default]
724 V3 = 3,
725}
726
727impl AsTomlValue for Resolver {
728 fn as_toml_value(&self) -> Item {
729 match self {
730 Self::V1 => "1".into(),
731 Self::V2 => "2".into(),
732 Self::V3 => "3".into(),
733 }
734 }
735}
736
737#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord, Merge, Default)]
739#[cfg_attr(feature = "schemars", derive(JsonSchema))]
740#[serde(default, rename_all = "kebab-case")]
741#[serde(deny_unknown_fields)]
742pub struct Product {
743 pub path: Option<String>,
745
746 pub name: Option<String>,
752
753 #[serde(skip_serializing_if = "Option::is_none")]
755 pub test: Option<bool>,
756
757 #[serde(skip_serializing_if = "Option::is_none")]
761 pub doctest: Option<bool>,
762
763 #[serde(skip_serializing_if = "Option::is_none")]
765 pub bench: Option<bool>,
766
767 #[serde(skip_serializing_if = "Option::is_none")]
769 pub doc: Option<bool>,
770
771 #[serde(
774 alias = "proc_macro",
775 alias = "proc-macro",
776 skip_serializing_if = "crate::is_false"
777 )]
778 #[merge(with = overwrite_if_true)]
779 pub proc_macro: bool,
780
781 #[serde(skip_serializing_if = "Option::is_none")]
785 pub harness: Option<bool>,
786
787 #[serde(skip_serializing_if = "BTreeSet::is_empty")]
789 pub crate_type: BTreeSet<String>,
790
791 #[serde(skip_serializing_if = "BTreeSet::is_empty")]
796 pub required_features: BTreeSet<String>,
797}
798
799impl AsTomlValue for Product {
800 fn as_toml_value(&self) -> Item {
801 let mut table = Table::new();
802
803 add_string!(self, table => path, name);
804 add_bool!(self, table => proc_macro);
805 add_string_list!(self, table => crate_type, required_features);
806
807 add_if_false!(self, table => test, doctest, bench, doc, harness);
808
809 table.into()
810 }
811}