#[macro_use]
mod macros;
mod toml_helpers;
use toml_helpers::*;
mod package;
pub use package::*;
mod profile_settings;
pub use profile_settings::*;
mod workspace;
pub use workspace::*;
use merge_it::*;
#[cfg(feature = "schemars")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::{BTreeMap, BTreeSet};
use std::mem;
use std::path::PathBuf;
use toml_edit::{Array, ArrayOfTables, DocumentMut, InlineTable, Item, Table, Value as TomlValue};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Merge, Default)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub struct Manifest {
#[merge(with = merge_options)]
pub workspace: Option<Workspace>,
#[merge(with = merge_options)]
pub package: Option<Package>,
#[merge(with = merge_options)]
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lib: Option<Product>,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
pub bin: BTreeSet<Product>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub target: BTreeMap<String, Target>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub patch: BTreeMap<String, BTreeMap<String, Dependency>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[merge(with = merge_options)]
pub profile: Option<Profiles>,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
pub bench: BTreeSet<Product>,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
pub test: BTreeSet<Product>,
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
pub example: BTreeSet<Product>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lints: Option<Inheritable<Lints>>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub features: BTreeMap<String, BTreeSet<String>>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
#[merge(with = merge_btree_maps)]
pub dependencies: BTreeMap<String, Dependency>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
#[merge(with = merge_btree_maps)]
pub dev_dependencies: BTreeMap<String, Dependency>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
#[merge(with = merge_btree_maps)]
pub build_dependencies: BTreeMap<String, Dependency>,
}
impl Manifest {
pub fn as_document(&self) -> DocumentMut {
let mut document = DocumentMut::new();
if let Some(workspace) = &self.workspace {
document.insert("workspace", workspace.as_toml_value());
}
add_value!(self, document => package, lib, profile);
if !self.target.is_empty() {
let mut table = Table::from_iter(
self.target
.iter()
.map(|(name, target)| (toml_edit::Key::from(name), target.as_toml_value())),
);
table.set_implicit(true);
document["target"] = table.into();
}
if !self.bin.is_empty() {
let array =
ArrayOfTables::from_iter(self.bin.iter().map(|i| match i.as_toml_value() {
Item::Table(table) => table,
_ => panic!("Found non-tables for cargo toml bin"),
}));
document["bin"] = array.into();
}
if !self.bench.is_empty() {
let array = ArrayOfTables::from_iter(self.bench.iter().map(
|i| match i.as_toml_value() {
Item::Table(table) => table,
_ => panic!("Found non-tables for cargo toml bench"),
},
));
document["bench"] = array.into();
}
if !self.test.is_empty() {
let array =
ArrayOfTables::from_iter(self.test.iter().map(|i| match i.as_toml_value() {
Item::Table(table) => table,
_ => panic!("Found non-tables for cargo toml test"),
}));
document["test"] = array.into();
}
if !self.example.is_empty() {
let array = ArrayOfTables::from_iter(self.example.iter().map(
|i| match i.as_toml_value() {
Item::Table(table) => table,
_ => panic!("Found non-tables for cargo toml examples"),
},
));
document["example"] = array.into();
}
if let Some(lints) = &self.lints {
document["lints"] = match lints {
Inheritable::Workspace { workspace } => {
Table::from_iter([("workspace", *workspace)]).into()
}
Inheritable::Value(lints) => lints.as_toml_value(),
};
}
add_table!(self, document => dev_dependencies, build_dependencies, dependencies);
if !self.patch.is_empty() {
let mut table = Table::from_iter(self.patch.iter().map(|(name, deps)| {
let mut deps_table =
Table::from_iter(deps.iter().map(|(dep_name, dep)| {
(toml_edit::Key::from(dep_name), dep.as_toml_value())
}));
deps_table.set_implicit(true);
(toml_edit::Key::from(name), deps_table)
}));
table.set_implicit(true);
document["patch"] = table.into();
}
if !self.features.is_empty() {
document["features"] =
Table::from_iter(self.features.iter().map(|(name, features)| {
let mut array = Array::from_iter(features);
format_array(&mut array);
(toml_edit::Key::from(name.as_str()), array)
}))
.into();
}
document
}
}
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
pub enum LintLevel {
Allow,
Warn,
Deny,
Forbid,
}
impl AsTomlValue for LintLevel {
fn as_toml_value(&self) -> Item {
let str = match self {
Self::Allow => "allow",
Self::Warn => "warn",
Self::Deny => "deny",
Self::Forbid => "forbid",
};
str.into()
}
}
#[track_caller]
fn item_to_toml_value(item: Item) -> Option<TomlValue> {
let output = match item {
Item::Value(value) => value,
Item::Table(table) => table.into_inline_table().into(),
Item::ArrayOfTables(arr) => arr.into_array().into(),
_ => return None,
};
Some(output)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Lint {
pub level: LintLevel,
pub priority: Option<i8>,
}
impl AsTomlValue for Lint {
fn as_toml_value(&self) -> Item {
let mut table = InlineTable::new();
if let Some(level) = item_to_toml_value(self.level.as_toml_value()) {
table.insert("level", level);
}
if let Some(priority) = self.priority {
table.insert("priority", i64::from(priority).into());
}
table.into()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(default, rename_all = "kebab-case")]
#[serde(deny_unknown_fields)]
pub struct Target {
pub dependencies: BTreeMap<String, Dependency>,
pub dev_dependencies: BTreeMap<String, Dependency>,
pub build_dependencies: BTreeMap<String, Dependency>,
}
impl AsTomlValue for Target {
fn as_toml_value(&self) -> Item {
let mut table = Table::new();
table.set_implicit(true);
add_table!(self, table => dev_dependencies, dependencies, build_dependencies);
table.into()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(untagged)]
pub enum Dependency {
Simple(String),
Inherited(InheritedDependencyDetail),
Detailed(Box<DependencyDetail>),
}
impl AsTomlValue for Dependency {
fn as_toml_value(&self) -> Item {
match self {
Self::Simple(ver) => ver.into(),
Self::Inherited(dep) => dep.as_toml_value(),
Self::Detailed(dep) => dep.as_toml_value(),
}
}
}
impl Dependency {
pub fn optional(&self) -> bool {
match self {
Self::Simple(_) => false,
Self::Inherited(dep) => dep.optional,
Self::Detailed(dep) => dep.optional,
}
}
pub fn features(&self) -> Option<&BTreeSet<String>> {
match self {
Self::Simple(_) => None,
Self::Inherited(dep) => Some(&dep.features),
Self::Detailed(dep) => Some(&dep.features),
}
}
pub const fn as_simple(&self) -> Option<&String> {
if let Self::Simple(v) = self {
Some(v)
} else {
None
}
}
pub const fn as_inherited(&self) -> Option<&InheritedDependencyDetail> {
if let Self::Inherited(v) = self {
Some(v)
} else {
None
}
}
pub const fn as_detailed(&self) -> Option<&DependencyDetail> {
if let Self::Detailed(v) = self {
Some(v)
} else {
None
}
}
}
impl Merge for Dependency {
fn merge(&mut self, other: Self) {
match self {
Self::Simple(left_simple) => {
match other {
Self::Simple(right_simple) => *left_simple = right_simple,
Self::Inherited(right_inherited) => *self = Self::Inherited(right_inherited),
Self::Detailed(mut right_detailed) => {
if right_detailed.version.is_none() {
let version = mem::take(left_simple);
right_detailed.version = Some(version);
}
*self = Self::Detailed(right_detailed);
}
};
}
Self::Inherited(left_inherited) => match other {
Self::Simple(right_simple) => {
let features = mem::take(&mut left_inherited.features);
*self = Self::Detailed(
DependencyDetail {
version: Some(right_simple),
optional: left_inherited.optional,
features,
..Default::default()
}
.into(),
);
}
Self::Inherited(right) => left_inherited.merge(right),
Self::Detailed(mut right) => {
if left_inherited.optional {
right.optional = true;
}
let left_features = mem::take(&mut left_inherited.features);
right.features.extend(left_features);
*self = Self::Detailed(right);
}
},
Self::Detailed(left_detailed) => match other {
Self::Simple(right_simple) => {
if left_detailed.version.is_none() {
left_detailed.version = Some(right_simple);
}
}
Self::Inherited(mut right) => {
if left_detailed.optional {
right.optional = true;
}
let left_features = mem::take(&mut left_detailed.features);
right.features.extend(left_features);
*self = Self::Inherited(right);
}
Self::Detailed(right) => {
left_detailed.merge(*right);
}
},
}
}
}
#[allow(clippy::trivially_copy_pass_by_ref)]
pub(crate) const fn is_false(boolean: &bool) -> bool {
!*boolean
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, Merge)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(rename_all = "kebab-case")]
#[serde(deny_unknown_fields)]
pub struct InheritedDependencyDetail {
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
pub features: BTreeSet<String>,
#[serde(default, skip_serializing_if = "crate::is_false")]
#[merge(with = overwrite_if_true)]
pub optional: bool,
#[serde(skip_serializing_if = "crate::is_false")]
#[merge(with = overwrite_if_true)]
pub workspace: bool,
}
impl AsTomlValue for InheritedDependencyDetail {
fn as_toml_value(&self) -> Item {
let mut table = InlineTable::new();
add_bool!(self, table => workspace, optional);
add_string_list!(self, table => features);
table.into()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Merge, Default)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(default, rename_all = "kebab-case")]
#[serde(deny_unknown_fields)]
pub struct DependencyDetail {
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub package: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub registry: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub registry_index: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub git: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub branch: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tag: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub rev: Option<String>,
#[serde(skip_serializing_if = "BTreeSet::is_empty")]
#[merge(with = BTreeSet::extend)]
pub features: BTreeSet<String>,
#[serde(skip_serializing_if = "crate::is_false")]
#[merge(with = overwrite_if_true)]
pub optional: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_features: Option<bool>,
}
impl AsTomlValue for DependencyDetail {
fn as_toml_value(&self) -> Item {
let mut table = InlineTable::new();
add_string!(self, table => version, path, package, registry, registry_index, git, branch, tag, rev);
add_bool!(self, table => optional);
add_if_false!(self, table => default_features);
add_string_list!(self, table => features);
table.into()
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(untagged)]
pub enum Inheritable<T> {
#[serde(rename = "workspace")]
Workspace {
workspace: bool,
},
Value(T),
}
impl<T> Inheritable<T> {
pub const fn is_workspace(&self) -> bool {
matches!(self, Self::Workspace { workspace: true })
}
pub const fn as_value(&self) -> Option<&T> {
if let Self::Value(val) = self {
Some(val)
} else {
None
}
}
}
impl<T: Merge> Merge for Inheritable<T> {
fn merge(&mut self, other: Self) {
match self {
Self::Workspace { .. } => {
*self = other;
}
Self::Value(content_left) => {
match other {
Self::Workspace { workspace } => *self = Self::Workspace { workspace },
Self::Value(content_right) => content_left.merge(content_right),
};
}
}
}
}
impl<T: AsTomlValue> AsTomlValue for Inheritable<T> {
fn as_toml_value(&self) -> Item {
match self {
Self::Workspace { workspace } => {
InlineTable::from_iter([("workspace", *workspace)]).into()
}
Self::Value(value) => value.as_toml_value(),
}
}
}
impl<T: Default> Default for Inheritable<T> {
fn default() -> Self {
Self::Value(T::default())
}
}
impl<T: Default + PartialEq> Inheritable<T> {
pub fn is_default(&self) -> bool {
match self {
Self::Workspace { .. } => false,
Self::Value(v) => T::default() == *v,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
pub enum Edition {
#[default]
#[serde(rename = "2015")]
E2015 = 2015,
#[serde(rename = "2018")]
E2018 = 2018,
#[serde(rename = "2021")]
E2021 = 2021,
#[serde(rename = "2024")]
E2024 = 2024,
}
impl AsTomlValue for Edition {
fn as_toml_value(&self) -> Item {
let str = match self {
Self::E2015 => "2015",
Self::E2018 => "2018",
Self::E2021 => "2021",
Self::E2024 => "2024",
};
str.into()
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(
untagged,
expecting = "the value should be either a boolean or a file path"
)]
pub enum OptionalFile {
Flag(bool),
Path(PathBuf),
}
impl AsTomlValue for OptionalFile {
fn as_toml_value(&self) -> Item {
match self {
Self::Flag(bool) => (*bool).into(),
Self::Path(path) => path.to_string_lossy().as_ref().into(),
}
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(
untagged,
expecting = "the value should be either a boolean, or an array of registry names"
)]
pub enum Publish {
Flag(bool),
Registry(BTreeSet<String>),
}
impl AsTomlValue for Publish {
fn as_toml_value(&self) -> Item {
match self {
Self::Flag(bool) => (*bool).into(),
Self::Registry(list) => toml_string_list(list),
}
}
}
#[derive(Debug, Default, PartialEq, Eq, Ord, PartialOrd, Copy, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(expecting = "if there's a newer resolver, then this parser has to be updated")]
#[repr(u8)]
pub enum Resolver {
#[serde(rename = "1")]
V1 = 1,
#[serde(rename = "2")]
V2 = 2,
#[serde(rename = "3")]
#[default]
V3 = 3,
}
impl AsTomlValue for Resolver {
fn as_toml_value(&self) -> Item {
match self {
Self::V1 => "1".into(),
Self::V2 => "2".into(),
Self::V3 => "3".into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord, Merge, Default)]
#[cfg_attr(feature = "schemars", derive(JsonSchema))]
#[serde(default, rename_all = "kebab-case")]
#[serde(deny_unknown_fields)]
pub struct Product {
pub path: Option<String>,
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub test: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub doctest: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bench: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub doc: Option<bool>,
#[serde(
alias = "proc_macro",
alias = "proc-macro",
skip_serializing_if = "crate::is_false"
)]
#[merge(with = overwrite_if_true)]
pub proc_macro: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub harness: Option<bool>,
#[serde(skip_serializing_if = "BTreeSet::is_empty")]
pub crate_type: BTreeSet<String>,
#[serde(skip_serializing_if = "BTreeSet::is_empty")]
pub required_features: BTreeSet<String>,
}
impl AsTomlValue for Product {
fn as_toml_value(&self) -> Item {
let mut table = Table::new();
add_string!(self, table => path, name);
add_bool!(self, table => proc_macro);
add_string_list!(self, table => crate_type, required_features);
add_if_false!(self, table => test, doctest, bench, doc, harness);
table.into()
}
}