use std::collections::BTreeMap;
use axoasset::{toml_edit, SourceFile};
use axoproject::local_repo::LocalRepo;
use camino::{Utf8Path, Utf8PathBuf};
use cargo_dist_schema::{
AptPackageName, ChecksumExtensionRef, ChocolateyPackageName, GithubAttestationsFilters,
GithubAttestationsPhase, HomebrewPackageName, PackageVersion, TripleName, TripleNameRef,
};
use serde::{Deserialize, Serialize};
use crate::announce::TagSettings;
use crate::SortedMap;
use crate::{
errors::{DistError, DistResult},
METADATA_DIST,
};
pub mod v0;
pub mod v0_to_v1;
pub mod v1;
pub use v0::{DistMetadata, GenericConfig};
pub type GithubPermissionMap = SortedMap<String, GithubPermission>;
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, PartialOrd, Ord, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum GithubPermission {
Read,
Write,
Admin,
}
#[derive(Debug, Clone)]
pub struct Config {
pub tag_settings: TagSettings,
pub create_hosting: bool,
pub artifact_mode: ArtifactMode,
pub no_local_paths: bool,
pub allow_all_dirty: bool,
pub targets: Vec<TripleName>,
pub ci: Vec<CiStyle>,
pub installers: Vec<InstallerStyle>,
pub root_cmd: String,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ArtifactMode {
Local,
Global,
Host,
All,
Lies,
}
impl std::fmt::Display for ArtifactMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let string = match self {
ArtifactMode::Local => "local",
ArtifactMode::Global => "global",
ArtifactMode::Host => "host",
ArtifactMode::All => "all",
ArtifactMode::Lies => "lies",
};
string.fmt(f)
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "kebab-case")]
pub enum CiStyle {
Github,
}
impl CiStyle {
pub(crate) fn native_hosting(&self) -> Option<HostingStyle> {
match self {
CiStyle::Github => Some(HostingStyle::Github),
}
}
}
impl std::fmt::Display for CiStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let string = match self {
CiStyle::Github => "github",
};
string.fmt(f)
}
}
impl std::str::FromStr for CiStyle {
type Err = DistError;
fn from_str(val: &str) -> DistResult<Self> {
let res = match val {
"github" => CiStyle::Github,
s => {
return Err(DistError::UnrecognizedCiStyle {
style: s.to_string(),
})
}
};
Ok(res)
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum LibraryStyle {
#[serde(rename = "cdylib")]
CDynamic,
#[serde(rename = "cstaticlib")]
CStatic,
}
impl std::fmt::Display for LibraryStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let string = match self {
Self::CDynamic => "cdylib",
Self::CStatic => "cstaticlib",
};
string.fmt(f)
}
}
impl std::str::FromStr for LibraryStyle {
type Err = DistError;
fn from_str(val: &str) -> DistResult<Self> {
let res = match val {
"cdylib" => Self::CDynamic,
"cstaticlib" => Self::CStatic,
s => {
return Err(DistError::UnrecognizedLibraryStyle {
style: s.to_string(),
})
}
};
Ok(res)
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum InstallerStyle {
Shell,
Powershell,
Npm,
Homebrew,
Msi,
Pkg,
}
impl std::fmt::Display for InstallerStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let string = match self {
InstallerStyle::Shell => "shell",
InstallerStyle::Powershell => "powershell",
InstallerStyle::Npm => "npm",
InstallerStyle::Homebrew => "homebrew",
InstallerStyle::Msi => "msi",
InstallerStyle::Pkg => "pkg",
};
string.fmt(f)
}
}
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum GithubReleasePhase {
#[default]
Auto,
Host,
Announce,
}
impl std::fmt::Display for GithubReleasePhase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let string = match self {
GithubReleasePhase::Auto => "auto",
GithubReleasePhase::Host => "host",
GithubReleasePhase::Announce => "announce",
};
string.fmt(f)
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum HostingStyle {
Github,
Simple,
}
impl std::fmt::Display for HostingStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let string = match self {
HostingStyle::Github => "github",
HostingStyle::Simple => "simple",
};
string.fmt(f)
}
}
impl std::str::FromStr for HostingStyle {
type Err = DistError;
fn from_str(val: &str) -> DistResult<Self> {
let res = match val {
"github" => HostingStyle::Github,
"simple" => HostingStyle::Simple,
s => {
return Err(DistError::UnrecognizedHostingStyle {
style: s.to_string(),
})
}
};
Ok(res)
}
}
#[derive(Clone, Debug, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum PublishStyle {
Homebrew,
Npm,
User(String),
}
impl std::str::FromStr for PublishStyle {
type Err = DistError;
fn from_str(s: &str) -> DistResult<Self> {
if let Some(slug) = s.strip_prefix("./") {
Ok(Self::User(slug.to_owned()))
} else if s == "homebrew" {
Ok(Self::Homebrew)
} else if s == "npm" {
Ok(Self::Npm)
} else {
Err(DistError::UnrecognizedJobStyle {
style: s.to_owned(),
})
}
}
}
impl<'de> serde::Deserialize<'de> for PublishStyle {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let path = String::deserialize(deserializer)?;
path.parse().map_err(|e| D::Error::custom(format!("{e}")))
}
}
impl std::fmt::Display for PublishStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PublishStyle::Homebrew => write!(f, "homebrew"),
PublishStyle::Npm => write!(f, "npm"),
PublishStyle::User(s) => write!(f, "./{s}"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum JobStyle {
User(String),
}
impl std::str::FromStr for JobStyle {
type Err = DistError;
fn from_str(s: &str) -> DistResult<Self> {
if let Some(slug) = s.strip_prefix("./") {
Ok(Self::User(slug.to_owned()))
} else {
Err(DistError::UnrecognizedJobStyle {
style: s.to_owned(),
})
}
}
}
impl serde::Serialize for JobStyle {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let s = self.to_string();
s.serialize(serializer)
}
}
impl<'de> serde::Deserialize<'de> for JobStyle {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let path = String::deserialize(deserializer)?;
path.parse().map_err(|e| D::Error::custom(format!("{e}")))
}
}
impl std::fmt::Display for JobStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
JobStyle::User(s) => write!(f, "./{s}"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ZipStyle {
Zip,
Tar(CompressionImpl),
TempDir,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum CompressionImpl {
Gzip,
Xzip,
Zstd,
}
impl ZipStyle {
pub fn ext(&self) -> &'static str {
match self {
ZipStyle::TempDir => "",
ZipStyle::Zip => ".zip",
ZipStyle::Tar(compression) => match compression {
CompressionImpl::Gzip => ".tar.gz",
CompressionImpl::Xzip => ".tar.xz",
CompressionImpl::Zstd => ".tar.zst",
},
}
}
}
impl Serialize for ZipStyle {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.ext())
}
}
impl<'de> Deserialize<'de> for ZipStyle {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let ext = String::deserialize(deserializer)?;
match &*ext {
".zip" => Ok(ZipStyle::Zip),
".tar.gz" => Ok(ZipStyle::Tar(CompressionImpl::Gzip)),
".tar.xz" => Ok(ZipStyle::Tar(CompressionImpl::Xzip)),
".tar.zstd" | ".tar.zst" => Ok(ZipStyle::Tar(CompressionImpl::Zstd)),
_ => Err(D::Error::custom(format!(
"unknown archive format {ext}, expected one of: .zip, .tar.gz, .tar.xz, .tar.zstd, .tar.zst"
))),
}
}
}
const CARGO_HOME_INSTALL_PATH: &str = "CARGO_HOME";
#[derive(Debug, Clone, PartialEq)]
pub enum InstallPathStrategy {
CargoHome,
HomeSubdir {
subdir: String,
},
EnvSubdir {
env_key: String,
subdir: String,
},
}
impl InstallPathStrategy {
pub fn default_list() -> Vec<Self> {
vec![InstallPathStrategy::CargoHome]
}
}
impl std::str::FromStr for InstallPathStrategy {
type Err = DistError;
fn from_str(path: &str) -> DistResult<Self> {
if path == CARGO_HOME_INSTALL_PATH {
Ok(InstallPathStrategy::CargoHome)
} else if let Some(subdir) = path.strip_prefix("~/") {
if subdir.is_empty() {
Err(DistError::InstallPathHomeSubdir {
path: path.to_owned(),
})
} else {
Ok(InstallPathStrategy::HomeSubdir {
subdir: subdir.strip_suffix('/').unwrap_or(subdir).to_owned(),
})
}
} else if let Some(val) = path.strip_prefix('$') {
if let Some((env_key, subdir)) = val.split_once('/') {
Ok(InstallPathStrategy::EnvSubdir {
env_key: env_key.to_owned(),
subdir: subdir.strip_suffix('/').unwrap_or(subdir).to_owned(),
})
} else {
Err(DistError::InstallPathEnvSlash {
path: path.to_owned(),
})
}
} else {
Err(DistError::InstallPathInvalid {
path: path.to_owned(),
})
}
}
}
impl std::fmt::Display for InstallPathStrategy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
InstallPathStrategy::CargoHome => write!(f, "{}", CARGO_HOME_INSTALL_PATH),
InstallPathStrategy::HomeSubdir { subdir } => write!(f, "~/{subdir}"),
InstallPathStrategy::EnvSubdir { env_key, subdir } => write!(f, "${env_key}/{subdir}"),
}
}
}
impl serde::Serialize for InstallPathStrategy {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> serde::Deserialize<'de> for InstallPathStrategy {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let path = String::deserialize(deserializer)?;
path.parse().map_err(|e| D::Error::custom(format!("{e}")))
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct GithubRepoPair {
pub owner: String,
pub repo: String,
}
impl std::str::FromStr for GithubRepoPair {
type Err = DistError;
fn from_str(pair: &str) -> DistResult<Self> {
let Some((owner, repo)) = pair.split_once('/') else {
return Err(DistError::GithubRepoPairParse {
pair: pair.to_owned(),
});
};
Ok(GithubRepoPair {
owner: owner.to_owned(),
repo: repo.to_owned(),
})
}
}
impl std::fmt::Display for GithubRepoPair {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}/{}", self.owner, self.repo)
}
}
impl serde::Serialize for GithubRepoPair {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> serde::Deserialize<'de> for GithubRepoPair {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::de::Error;
let path = String::deserialize(deserializer)?;
path.parse().map_err(|e| D::Error::custom(format!("{e}")))
}
}
impl GithubRepoPair {
pub fn into_jinja(self) -> JinjaGithubRepoPair {
JinjaGithubRepoPair {
owner: self.owner,
repo: self.repo,
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct JinjaGithubRepoPair {
pub owner: String,
pub repo: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "kind")]
pub enum JinjaInstallPathStrategy {
CargoHome,
HomeSubdir {
subdir: String,
},
EnvSubdir {
env_key: String,
subdir: String,
},
}
impl InstallPathStrategy {
pub fn into_jinja(self) -> JinjaInstallPathStrategy {
match self {
InstallPathStrategy::CargoHome => JinjaInstallPathStrategy::CargoHome,
InstallPathStrategy::HomeSubdir { subdir } => {
JinjaInstallPathStrategy::HomeSubdir { subdir }
}
InstallPathStrategy::EnvSubdir { env_key, subdir } => {
JinjaInstallPathStrategy::EnvSubdir { env_key, subdir }
}
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ChecksumStyle {
Sha256,
Sha512,
Sha3_256,
Sha3_512,
Blake2s,
Blake2b,
False,
}
impl ChecksumStyle {
pub fn ext(self) -> &'static ChecksumExtensionRef {
ChecksumExtensionRef::from_str(match self {
ChecksumStyle::Sha256 => "sha256",
ChecksumStyle::Sha512 => "sha512",
ChecksumStyle::Sha3_256 => "sha3-256",
ChecksumStyle::Sha3_512 => "sha3-512",
ChecksumStyle::Blake2s => "blake2s",
ChecksumStyle::Blake2b => "blake2b",
ChecksumStyle::False => "false",
})
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum GenerateMode {
#[serde(rename = "ci")]
Ci,
#[serde(rename = "msi")]
Msi,
}
impl std::fmt::Display for GenerateMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GenerateMode::Ci => "ci".fmt(f),
GenerateMode::Msi => "msi".fmt(f),
}
}
}
#[derive(Clone, Debug)]
pub struct HostArgs {
pub steps: Vec<HostStyle>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum HostStyle {
Check,
Create,
Upload,
Release,
Announce,
}
impl std::fmt::Display for HostStyle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let string = match self {
HostStyle::Check => "check",
HostStyle::Create => "create",
HostStyle::Upload => "upload",
HostStyle::Release => "release",
HostStyle::Announce => "announce",
};
string.fmt(f)
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct MacPkgConfig {
pub identifier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub install_location: Option<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct SystemDependencies {
#[serde(default)]
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub homebrew: BTreeMap<HomebrewPackageName, SystemDependency>,
#[serde(default)]
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub apt: BTreeMap<AptPackageName, SystemDependency>,
#[serde(default)]
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub chocolatey: BTreeMap<ChocolateyPackageName, SystemDependency>,
}
impl SystemDependencies {
pub fn append(&mut self, other: &mut Self) {
self.homebrew.append(&mut other.homebrew);
self.apt.append(&mut other.apt);
self.chocolatey.append(&mut other.chocolatey);
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)]
pub struct SystemDependency(pub SystemDependencyComplex);
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
pub struct SystemDependencyComplex {
pub version: Option<PackageVersion>,
#[serde(default)]
pub stage: Vec<DependencyKind>,
#[serde(default)]
pub targets: Vec<TripleName>,
}
impl SystemDependencyComplex {
pub fn wanted_for_target(&self, target: &TripleNameRef) -> bool {
if self.targets.is_empty() {
true
} else {
self.targets.iter().any(|t| t == target)
}
}
pub fn stage_wanted(&self, stage: &DependencyKind) -> bool {
if self.stage.is_empty() {
match stage {
DependencyKind::Build => true,
DependencyKind::Run => false,
}
} else {
self.stage.contains(stage)
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SystemDependencyKind {
Untagged(String),
Tagged(SystemDependencyComplex),
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum DependencyKind {
Build,
Run,
}
impl std::fmt::Display for DependencyKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DependencyKind::Build => "build".fmt(f),
DependencyKind::Run => "run".fmt(f),
}
}
}
impl<'de> Deserialize<'de> for SystemDependency {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let kind: SystemDependencyKind = SystemDependencyKind::deserialize(deserializer)?;
let res = match kind {
SystemDependencyKind::Untagged(version) => {
let v = if version == "*" { None } else { Some(version) };
SystemDependencyComplex {
version: v.map(PackageVersion::new),
stage: vec![],
targets: vec![],
}
}
SystemDependencyKind::Tagged(dep) => dep,
};
Ok(SystemDependency(res))
}
}
#[derive(Debug, Clone)]
pub enum DirtyMode {
AllowList(Vec<GenerateMode>),
AllowAll,
}
impl DirtyMode {
pub fn should_run(&self, mode: GenerateMode) -> bool {
match self {
DirtyMode::AllowAll => false,
DirtyMode::AllowList(list) => !list.contains(&mode),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ProductionMode {
Test,
Prod,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct ExtraArtifact {
#[serde(default)]
#[serde(skip_serializing_if = "path_is_empty")]
pub working_dir: Utf8PathBuf,
#[serde(rename = "build")]
pub command: Vec<String>,
#[serde(rename = "artifacts")]
pub artifact_relpaths: Vec<Utf8PathBuf>,
}
fn path_is_empty(p: &Utf8PathBuf) -> bool {
p.as_str().is_empty()
}
impl std::fmt::Display for ProductionMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ProductionMode::Test => "test".fmt(f),
ProductionMode::Prod => "prod".fmt(f),
}
}
}
pub(crate) fn parse_metadata_table_or_manifest(
manifest_path: &Utf8Path,
dist_manifest_path: Option<&Utf8Path>,
metadata_table: Option<&serde_json::Value>,
) -> DistResult<DistMetadata> {
if let Some(dist_manifest_path) = dist_manifest_path {
reject_metadata_table(manifest_path, dist_manifest_path, metadata_table)?;
let src = SourceFile::load_local(dist_manifest_path)?;
parse_generic_config(src)
} else {
parse_metadata_table(manifest_path, metadata_table)
}
}
pub(crate) fn parse_generic_config(src: SourceFile) -> DistResult<DistMetadata> {
let config: GenericConfig = src.deserialize_toml()?;
Ok(config.dist.unwrap_or_default())
}
pub(crate) fn reject_metadata_table(
manifest_path: &Utf8Path,
dist_manifest_path: &Utf8Path,
metadata_table: Option<&serde_json::Value>,
) -> DistResult<()> {
let has_dist_metadata = metadata_table.and_then(|t| t.get(METADATA_DIST)).is_some();
if has_dist_metadata {
Err(DistError::UnusedMetadata {
manifest_path: manifest_path.to_owned(),
dist_manifest_path: dist_manifest_path.to_owned(),
})
} else {
Ok(())
}
}
pub(crate) fn parse_metadata_table(
manifest_path: &Utf8Path,
metadata_table: Option<&serde_json::Value>,
) -> DistResult<DistMetadata> {
Ok(metadata_table
.and_then(|t| t.get(METADATA_DIST))
.map(DistMetadata::deserialize)
.transpose()
.map_err(|cause| DistError::CargoTomlParse {
manifest_path: manifest_path.to_owned(),
cause,
})?
.unwrap_or_default())
}
pub fn get_project() -> Result<axoproject::WorkspaceGraph, axoproject::errors::ProjectError> {
let start_dir = std::env::current_dir().expect("couldn't get current working dir!?");
let start_dir = Utf8PathBuf::from_path_buf(start_dir).expect("project path isn't utf8!?");
let repo = LocalRepo::new("git", &start_dir).ok();
let workspaces = axoproject::WorkspaceGraph::find_from_git(&start_dir, repo)?;
Ok(workspaces)
}
pub fn load_toml(manifest_path: &Utf8Path) -> DistResult<toml_edit::DocumentMut> {
let src = axoasset::SourceFile::load_local(manifest_path)?;
let toml = src.deserialize_toml_edit()?;
Ok(toml)
}
pub fn write_toml(manifest_path: &Utf8Path, toml: toml_edit::DocumentMut) -> DistResult<()> {
let toml_text = toml.to_string();
axoasset::LocalAsset::write_new(&toml_text, manifest_path)?;
Ok(())
}
pub fn get_toml_metadata(
toml: &mut toml_edit::DocumentMut,
is_workspace: bool,
) -> &mut toml_edit::Item {
let root_key = if is_workspace { "workspace" } else { "package" };
let workspace = toml[root_key].or_insert(toml_edit::table());
if let Some(t) = workspace.as_table_mut() {
t.set_implicit(true)
}
let metadata = workspace["metadata"].or_insert(toml_edit::table());
if let Some(t) = metadata.as_table_mut() {
t.set_implicit(true)
}
metadata
}
mod opt_string_or_vec {
use super::*;
use serde::de::Error;
pub fn serialize<S, T>(v: &Option<Vec<T>>, s: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
T: std::fmt::Display,
{
let Some(vec) = v else {
return s.serialize_none();
};
if vec.len() == 1 {
s.serialize_str(&vec[0].to_string())
} else {
let string_vec = Vec::from_iter(vec.iter().map(ToString::to_string));
string_vec.serialize(s)
}
}
pub fn deserialize<'de, D, T>(deserializer: D) -> Result<Option<Vec<T>>, D::Error>
where
D: serde::Deserializer<'de>,
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
struct StringOrVec<T>(std::marker::PhantomData<T>);
impl<'de, T> serde::de::Visitor<'de> for StringOrVec<T>
where
T: std::str::FromStr,
T::Err: std::fmt::Display,
{
type Value = Option<Vec<T>>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("string or list of strings")
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: Error,
{
Ok(None)
}
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
where
E: Error,
{
Ok(Some(vec![s
.parse()
.map_err(|e| E::custom(format!("{e}")))?]))
}
fn visit_seq<S>(self, seq: S) -> Result<Self::Value, S::Error>
where
S: serde::de::SeqAccess<'de>,
{
let vec: Vec<String> =
Deserialize::deserialize(serde::de::value::SeqAccessDeserializer::new(seq))?;
let parsed: Result<Vec<T>, S::Error> = vec
.iter()
.map(|s| s.parse::<T>().map_err(|e| S::Error::custom(format!("{e}"))))
.collect();
Ok(Some(parsed?))
}
}
deserializer.deserialize_any(StringOrVec::<T>(std::marker::PhantomData))
}
}