use serde::{Deserialize, Deserializer, Serialize};
use std::collections::BTreeMap;
use std::sync::OnceLock;
fn global_registry() -> &'static BTreeMap<String, String> {
static REGISTRY: OnceLock<BTreeMap<String, String>> = OnceLock::new();
REGISTRY.get_or_init(|| {
let path = crate::home_dir().join(".ym").join("registry.json");
if let Ok(content) = std::fs::read_to_string(&path) {
let forward: BTreeMap<String, String> =
serde_json::from_str(&content).unwrap_or_default();
forward.into_iter().map(|(coord, alias)| (alias, coord)).collect()
} else {
BTreeMap::new()
}
})
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(untagged)]
pub enum DependencyValue {
Simple(String),
Detailed(DependencySpec),
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct DependencySpec {
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub classifier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exclude: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub git: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", rename = "ref")]
pub git_ref: Option<String>,
}
impl DependencyValue {
pub fn version(&self) -> Option<&str> {
match self {
DependencyValue::Simple(v) => Some(v),
DependencyValue::Detailed(spec) => spec.version.as_deref(),
}
}
pub fn scope(&self) -> &str {
match self {
DependencyValue::Simple(_) => "compile",
DependencyValue::Detailed(spec) => spec.scope.as_deref().unwrap_or("compile"),
}
}
pub fn is_workspace(&self) -> bool {
match self {
DependencyValue::Simple(_) => false,
DependencyValue::Detailed(spec) => spec.workspace.unwrap_or(false),
}
}
pub fn classifier(&self) -> Option<&str> {
match self {
DependencyValue::Simple(_) => None,
DependencyValue::Detailed(spec) => spec.classifier.as_deref(),
}
}
pub fn url(&self) -> Option<&str> {
match self {
DependencyValue::Simple(_) => None,
DependencyValue::Detailed(spec) => spec.url.as_deref(),
}
}
pub fn git(&self) -> Option<&str> {
match self {
DependencyValue::Simple(_) => None,
DependencyValue::Detailed(spec) => spec.git.as_deref(),
}
}
pub fn git_ref(&self) -> Option<&str> {
match self {
DependencyValue::Simple(_) => None,
DependencyValue::Detailed(spec) => spec.git_ref.as_deref(),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(untagged)]
pub enum RegistryValue {
Simple(String),
Detailed(RegistrySpec),
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct RegistrySpec {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub password: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(untagged)]
pub enum ResolutionValue {
Version(String),
Exclude(bool),
}
impl ResolutionValue {
pub fn is_excluded(&self) -> bool {
matches!(self, ResolutionValue::Exclude(false))
}
pub fn version(&self) -> Option<&str> {
match self {
ResolutionValue::Version(v) => Some(v),
_ => None,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(untagged)]
pub enum ScriptValue {
Simple(String),
Detailed(ScriptSpec),
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ScriptSpec {
pub command: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub timeout: Option<String>,
}
impl ScriptValue {
pub fn command(&self) -> &str {
match self {
ScriptValue::Simple(s) => s,
ScriptValue::Detailed(spec) => &spec.command,
}
}
pub fn timeout_secs(&self) -> Option<u64> {
match self {
ScriptValue::Simple(_) => None,
ScriptValue::Detailed(spec) => spec.timeout.as_ref().and_then(|t| parse_duration_secs(t)),
}
}
}
fn parse_duration_secs(s: &str) -> Option<u64> {
let s = s.trim();
if let Some(minutes) = s.strip_suffix('m') {
minutes.parse::<u64>().ok().map(|m| m * 60)
} else if let Some(seconds) = s.strip_suffix('s') {
seconds.parse::<u64>().ok()
} else if let Some(hours) = s.strip_suffix('h') {
hours.parse::<u64>().ok().map(|h| h * 3600)
} else {
s.parse::<u64>().ok() }
}
impl std::fmt::Display for DependencyValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DependencyValue::Simple(v) => write!(f, "{}", v),
DependencyValue::Detailed(spec) => {
if let Some(ref v) = spec.version {
write!(f, "{}", v)?;
if let Some(ref s) = spec.scope {
write!(f, " ({})", s)?;
}
} else if spec.workspace.unwrap_or(false) {
write!(f, "workspace")?;
} else {
write!(f, "*")?;
}
Ok(())
}
}
}
}
impl std::fmt::Display for RegistryValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RegistryValue::Simple(url) => write!(f, "{}", url),
RegistryValue::Detailed(spec) => write!(f, "{}", spec.url),
}
}
}
impl RegistryValue {
pub fn url(&self) -> &str {
match self {
RegistryValue::Simple(url) => url,
RegistryValue::Detailed(spec) => &spec.url,
}
}
pub fn scope(&self) -> Option<&str> {
match self {
RegistryValue::Simple(_) => None,
RegistryValue::Detailed(spec) => spec.scope.as_deref(),
}
}
pub fn username(&self) -> Option<&str> {
match self {
RegistryValue::Simple(_) => None,
RegistryValue::Detailed(spec) => spec.username.as_deref(),
}
}
pub fn password(&self) -> Option<&str> {
match self {
RegistryValue::Simple(_) => None,
RegistryValue::Detailed(spec) => spec.password.as_deref(),
}
}
}
pub fn is_maven_dep(key: &str) -> bool {
key.contains(':') || key.starts_with('@')
}
pub fn artifact_id_from_key(key: &str) -> &str {
if key.starts_with('@') {
key.split('/').next_back().unwrap_or(key)
} else {
key.split(':').next_back().unwrap_or(key)
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct YmConfig {
pub name: String,
#[serde(default = "default_group_id")]
pub group_id: String,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_version_or_workspace",
default
)]
pub version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_string_or_int",
default
)]
pub target: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub private: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub main: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub package: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub license: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub env: Option<BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dependencies: Option<BTreeMap<String, DependencyValue>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dev_dependencies: Option<BTreeMap<String, DependencyValue>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub workspaces: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jvm_args: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scripts: Option<BTreeMap<String, ScriptValue>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resolutions: Option<BTreeMap<String, ResolutionValue>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub registries: Option<BTreeMap<String, RegistryValue>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jvm: Option<JvmConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub compiler: Option<CompilerConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hot_reload: Option<HotReloadConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub exclusions: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ext: Option<BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope_mapping: Option<BTreeMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_dir: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub test_dir: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub native: Option<NativeConfig>,
}
fn default_group_id() -> String {
"com.example".to_string()
}
impl YmConfig {
pub fn resolve_var(version: &str, root: &YmConfig) -> String {
if !version.contains("${") {
return version.to_string();
}
let mut result = version.to_string();
if result.contains("${project.version}") {
let v = root.version.as_deref().unwrap_or("0.0.0");
result = result.replace("${project.version}", v);
}
if result.contains("${project.groupId}") {
result = result.replace("${project.groupId}", &root.group_id);
}
if result.contains("${") {
if let Some(ref ext) = root.ext {
for (k, v) in ext {
let placeholder = format!("${{{}}}", k);
if result.contains(&placeholder) {
result = result.replace(&placeholder, v);
}
}
}
}
if result.contains("${") {
result = Self::resolve_env_vars(&result);
}
result
}
pub fn resolve_env_vars(s: &str) -> String {
let mut result = s.to_string();
while let Some(start) = result.find("${env.") {
let rest = &result[start + 6..];
if let Some(end) = rest.find('}') {
let var_name = &rest[..end];
let value = std::env::var(var_name).unwrap_or_default();
result = result.replace(&format!("${{env.{}}}", var_name), &value);
} else {
break;
}
}
let mut out = String::new();
let mut remaining = result.as_str();
while let Some(start) = remaining.find("${") {
out.push_str(&remaining[..start]);
let rest = &remaining[start + 2..];
if let Some(end) = rest.find('}') {
let var_name = &rest[..end];
if !var_name.is_empty() && var_name.chars().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_') {
let value = std::env::var(var_name).unwrap_or_default();
out.push_str(&value);
} else {
out.push_str(&remaining[start..start + 2 + end + 1]);
}
remaining = &rest[end + 1..];
} else {
out.push_str(&remaining[start..]);
remaining = "";
break;
}
}
out.push_str(remaining);
out
}
fn iter_all_deps(&self) -> impl Iterator<Item = (&str, &DependencyValue, bool)> {
let deps = self.dependencies.iter()
.flat_map(|m| m.iter())
.map(|(k, v)| (k.as_str(), v, false));
let dev_deps = self.dev_dependencies.iter()
.flat_map(|m| m.iter())
.map(|(k, v)| (k.as_str(), v, true));
deps.chain(dev_deps)
}
fn effective_scope<'a>(value: &'a DependencyValue, is_dev: bool) -> &'a str {
if is_dev { "provided" } else { value.scope() }
}
pub fn resolved_resolutions(&self, root: &YmConfig) -> BTreeMap<String, String> {
match self.resolutions {
Some(ref res) => res
.iter()
.filter_map(|(k, v)| v.version().map(|ver| (self.resolve_key(k), Self::resolve_var(ver, root))))
.collect(),
None => BTreeMap::new(),
}
}
pub fn resolved_exclusions(&self) -> Vec<String> {
match self.resolutions {
Some(ref res) => res
.iter()
.filter(|(_, v)| v.is_excluded())
.map(|(k, _)| self.resolve_key(k))
.collect(),
None => Vec::new(),
}
}
pub fn resolve_key(&self, key: &str) -> String {
if key.starts_with('@') {
if let Some(ref mapping) = self.scope_mapping {
if let Some(coord) = mapping.get(key) {
if coord.contains(':') {
return coord.clone();
}
}
if let Some(slash_idx) = key.find('/') {
let scope = &key[..slash_idx];
let name = &key[slash_idx + 1..];
if let Some(group_id) = mapping.get(scope) {
return format!("{}:{}", group_id, name);
}
}
}
if let Some(coord) = global_registry().get(key) {
return coord.clone();
}
}
key.to_string()
}
pub fn maven_dependencies(&self) -> BTreeMap<String, String> {
let mut result = BTreeMap::new();
for (key, value, _is_dev) in self.iter_all_deps() {
if !is_maven_dep(key) { continue; }
if value.is_workspace() { continue; }
if value.url().is_some() || value.git().is_some() { continue; }
if let Some(version) = value.version() {
let resolved = self.resolve_key(key);
let coord_key = if let Some(classifier) = value.classifier() {
format!("{}:{}", resolved, classifier)
} else {
resolved
};
result.insert(coord_key, Self::resolve_var(version, self));
}
}
result
}
pub fn workspace_module_deps(&self) -> Vec<String> {
let mut result = Vec::new();
for (key, value, _is_dev) in self.iter_all_deps() {
if !is_maven_dep(key) && value.is_workspace() {
result.push(key.to_string());
}
}
result
}
pub fn maven_dependencies_with_root(&self, root: &YmConfig) -> BTreeMap<String, String> {
let mut result = BTreeMap::new();
for (key, value, _is_dev) in self.iter_all_deps() {
if !is_maven_dep(key) { continue; }
if value.url().is_some() || value.git().is_some() { continue; }
let resolved = root.resolve_key(key);
if value.is_workspace() {
let root_version = root.find_dep_version(key);
if let Some(version) = root_version {
result.insert(resolved, Self::resolve_var(version, root));
}
continue;
}
if let Some(version) = value.version() {
result.insert(resolved, Self::resolve_var(version, root));
}
}
result
}
pub fn find_dep_version(&self, key: &str) -> Option<&str> {
self.dependencies.as_ref()
.and_then(|d| d.get(key))
.and_then(|v| v.version())
.or_else(|| {
self.dev_dependencies.as_ref()
.and_then(|d| d.get(key))
.and_then(|v| v.version())
})
}
pub fn maven_dependencies_for_scopes(&self, scopes: &[&str]) -> BTreeMap<String, String> {
let mut result = BTreeMap::new();
for (key, value, is_dev) in self.iter_all_deps() {
if !is_maven_dep(key) { continue; }
if value.url().is_some() || value.git().is_some() { continue; }
if value.is_workspace() { continue; }
let dep_scope = Self::effective_scope(value, is_dev);
if scopes.contains(&dep_scope) {
if let Some(version) = value.version() {
let resolved = self.resolve_key(key);
let coord_key = if let Some(classifier) = value.classifier() {
format!("{}:{}", resolved, classifier)
} else {
resolved
};
result.insert(coord_key, Self::resolve_var(version, self));
}
}
}
result
}
pub fn maven_dependencies_for_scopes_with_root(&self, scopes: &[&str], root: &YmConfig) -> BTreeMap<String, String> {
let mut result = BTreeMap::new();
for (key, value, is_dev) in self.iter_all_deps() {
if !is_maven_dep(key) { continue; }
if value.url().is_some() || value.git().is_some() { continue; }
let resolved = root.resolve_key(key);
if value.is_workspace() {
let dep_scope = Self::effective_scope(value, is_dev);
if scopes.contains(&dep_scope) {
if let Some(version) = root.find_dep_version(key) {
result.insert(resolved, Self::resolve_var(version, root));
}
}
continue;
}
let dep_scope = Self::effective_scope(value, is_dev);
if scopes.contains(&dep_scope) {
if let Some(version) = value.version() {
result.insert(resolved, Self::resolve_var(version, root));
}
}
}
result
}
pub fn url_dependencies(&self) -> Vec<(String, String, String)> {
let mut result = Vec::new();
if let Some(ref deps) = self.dependencies {
for (key, value) in deps {
if let Some(url) = value.url() {
result.push((key.clone(), url.to_string(), value.scope().to_string()));
}
}
}
result
}
pub fn git_dependencies(&self) -> Vec<(String, String, Option<String>, String)> {
let mut result = Vec::new();
if let Some(ref deps) = self.dependencies {
for (key, value) in deps {
if let Some(git) = value.git() {
result.push((
key.clone(),
git.to_string(),
value.git_ref().map(|s| s.to_string()),
value.scope().to_string(),
));
}
}
}
result
}
pub fn validate_workspace_deps(&self, root: &YmConfig) -> Vec<String> {
let mut errors = Vec::new();
if let Some(ref deps) = self.dependencies {
for (key, value) in deps {
if is_maven_dep(key) {
if value.is_workspace() {
if root.find_dep_version(key).is_none() {
errors.push(format!(
"Dependency '{}' uses {{ workspace = true }} but root ym.json has no version for it",
key
));
}
}
} else if !value.is_workspace() {
errors.push(format!(
"Dependency '{}' is not a Maven coordinate and has no {{ workspace = true }} — must be a workspace module reference",
key
));
}
}
}
errors
}
pub fn per_dependency_exclusions(&self) -> Vec<String> {
let mut result = Vec::new();
if let Some(ref deps) = self.dependencies {
for value in deps.values() {
if let DependencyValue::Detailed(spec) = value {
if let Some(ref excludes) = spec.exclude {
result.extend(excludes.iter().cloned());
}
}
}
}
result
}
pub fn dependency_fingerprint(&self) -> String {
use std::fmt::Write;
let mut data = String::new();
if let Some(ref deps) = self.dependencies {
for (k, v) in deps {
let _ = writeln!(data, "dep:{}={}", k, v);
}
}
if let Some(ref res) = self.resolutions {
for (k, v) in res {
match v {
ResolutionValue::Version(ver) => { let _ = writeln!(data, "res:{}={}", k, ver); },
ResolutionValue::Exclude(b) => { let _ = writeln!(data, "res:{}=exclude:{}", k, b); },
}
}
}
if let Some(ref exc) = self.exclusions {
for e in exc {
let _ = writeln!(data, "exc:{}", e);
}
}
if let Some(ref regs) = self.registries {
for (k, v) in regs {
let _ = writeln!(data, "reg:{}={}", k, v);
}
}
crate::compiler::incremental::hash_bytes(data.as_bytes())
}
pub fn registry_entries(&self) -> Vec<crate::workspace::resolver::RegistryEntry> {
let mut entries = Vec::new();
if let Some(ref registries) = self.registries {
for value in registries.values() {
let url = value.url();
let resolved_url = if url.contains("${") {
Self::resolve_env_vars(url)
} else {
url.to_string()
};
entries.push(crate::workspace::resolver::RegistryEntry {
url: resolved_url,
scope: value.scope().map(|s| s.to_string()),
username: value.username().map(|u| {
if u.contains("${") { Self::resolve_env_vars(u) } else { u.to_string() }
}),
password: value.password().map(|p| {
if p.contains("${") { Self::resolve_env_vars(p) } else { p.to_string() }
}),
});
}
}
entries
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct JvmConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub vendor: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_download: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct CompilerConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub encoding: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotation_processors: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lint: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub args: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resource_extensions: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resource_exclude: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jacoco_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub libs: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct HotReloadConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub enabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub watch_extensions: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(rename_all = "camelCase")]
pub struct NativeConfig {
#[serde(skip_serializing_if = "Option::is_none")]
pub args: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub docker_image: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct ResolvedCache {
pub version: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub config_hash: Option<String>,
pub dependencies: BTreeMap<String, ResolvedDependency>,
}
impl Default for ResolvedCache {
fn default() -> Self {
Self {
version: 1,
config_hash: None,
dependencies: BTreeMap::new(),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct ResolvedDependency {
#[serde(skip_serializing_if = "Option::is_none")]
pub sha256: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dependencies: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub scope: Option<String>,
}
fn deserialize_string_or_int<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de;
struct StringOrInt;
impl<'de> de::Visitor<'de> for StringOrInt {
type Value = Option<String>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string, integer, or { workspace = true }")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
Ok(Some(v.to_string()))
}
fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
Ok(Some(v))
}
fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
Ok(Some(v.to_string()))
}
fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
Ok(Some(v.to_string()))
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: de::MapAccess<'de>,
{
while let Some((key, _value)) = map.next_entry::<String, serde::de::IgnoredAny>()? {
let _ = key;
}
Ok(None)
}
fn visit_some<D2>(self, deserializer: D2) -> Result<Self::Value, D2::Error>
where
D2: Deserializer<'de>,
{
deserializer.deserialize_any(StringOrInt)
}
}
deserializer.deserialize_any(StringOrInt)
}
fn deserialize_version_or_workspace<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de;
struct VersionOrWorkspace;
impl<'de> de::Visitor<'de> for VersionOrWorkspace {
type Value = Option<String>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a version string or { workspace = true }")
}
fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
Ok(Some(v.to_string()))
}
fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
Ok(Some(v))
}
fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
Ok(None)
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: de::MapAccess<'de>,
{
while let Some((key, _value)) = map.next_entry::<String, serde::de::IgnoredAny>()? {
let _ = key;
}
Ok(None)
}
fn visit_some<D2>(self, deserializer: D2) -> Result<Self::Value, D2::Error>
where
D2: Deserializer<'de>,
{
deserializer.deserialize_any(VersionOrWorkspace)
}
}
deserializer.deserialize_any(VersionOrWorkspace)
}