use serde::{Deserialize, Serialize};
use super::type_ref::TypeRef;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum NewtypeContainer {
Optional,
Vec,
MapKey,
MapValue,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum NewtypeConversion {
#[default]
TupleField,
TransparentString {
from: String,
into: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
wrapper_is_clone: Option<bool>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NewtypeWrapper {
Tuple(String),
Explicit(Vec<NewtypeWrapperMetadata>),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct NewtypeWrapperMetadata {
pub rust_path: String,
pub conversion: NewtypeConversion,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub containers: Vec<NewtypeContainer>,
}
impl NewtypeWrapperMetadata {
pub fn transparent_string(
rust_path: impl Into<String>,
from: impl Into<String>,
into: impl Into<String>,
containers: Vec<NewtypeContainer>,
) -> Self {
Self {
rust_path: rust_path.into(),
conversion: NewtypeConversion::TransparentString {
from: from.into(),
into: into.into(),
wrapper_is_clone: None,
},
containers,
}
}
}
impl NewtypeWrapper {
const EXPLICIT_PREFIX: &'static str = "alef:newtype-conversions:v1:";
const LEGACY_EXPLICIT_PREFIX: &'static str = "alef:newtype-conversions:";
pub fn decode(value: &str) -> Result<Self, String> {
if let Some(json) = value.strip_prefix(Self::EXPLICIT_PREFIX) {
return Self::decode_explicit(json);
}
if let Some(json) = value.strip_prefix(Self::LEGACY_EXPLICIT_PREFIX) {
if json.starts_with('v') {
return Err("unsupported transparent newtype metadata version".to_string());
}
return Self::decode_explicit(json);
}
Ok(Self::Tuple(value.to_string()))
}
pub fn encode_explicit(paths: &[NewtypeWrapperMetadata]) -> String {
format!(
"{}{}",
Self::EXPLICIT_PREFIX,
serde_json::to_string(paths).expect("newtype conversion metadata must serialize")
)
}
pub fn tuple_path(&self) -> Option<&str> {
match self {
Self::Tuple(path) => Some(path),
Self::Explicit(_) => None,
}
}
pub fn explicit_paths(&self) -> &[NewtypeWrapperMetadata] {
match self {
Self::Tuple(_) => &[],
Self::Explicit(paths) => paths,
}
}
pub fn explicit_paths_with_clone(&self, wrapper_is_clone: bool) -> Vec<NewtypeWrapperMetadata> {
self.explicit_paths()
.iter()
.cloned()
.map(|mut metadata| {
if let NewtypeConversion::TransparentString {
wrapper_is_clone: value,
..
} = &mut metadata.conversion
{
*value = Some(wrapper_is_clone);
}
metadata
})
.collect()
}
pub fn validate_for_type(&self, ty: &TypeRef, outer_optional: bool) -> Result<(), String> {
let Self::Explicit(paths) = self else {
return Ok(());
};
for metadata in paths {
validate_newtype_path(ty, outer_optional, &metadata.containers)?;
}
Ok(())
}
fn decode_explicit(json: &str) -> Result<Self, String> {
let paths: Vec<NewtypeWrapperMetadata> =
serde_json::from_str(json).map_err(|error| format!("invalid transparent newtype metadata: {error}"))?;
if paths.is_empty() {
return Err("transparent newtype metadata must contain at least one conversion path".to_string());
}
for (index, path) in paths.iter().enumerate() {
if path.rust_path.is_empty() {
return Err("transparent newtype metadata contains an empty Rust path".to_string());
}
let NewtypeConversion::TransparentString { from, into, .. } = &path.conversion else {
continue;
};
if from.is_empty() || into.is_empty() {
return Err("transparent newtype metadata contains an empty conversion method".to_string());
}
if paths[..index]
.iter()
.any(|previous| previous.containers == path.containers)
{
return Err(format!(
"transparent newtype metadata contains duplicate container path {:?}",
path.containers
));
}
}
validate_newtype_path_shape(&paths)?;
Ok(Self::Explicit(paths))
}
}
fn validate_newtype_path_shape(paths: &[NewtypeWrapperMetadata]) -> Result<(), String> {
if paths.len() == 1 && paths[0].containers.is_empty() {
return Ok(());
}
if paths.iter().any(|path| path.containers.is_empty()) {
return Err("transparent newtype metadata overlaps a leaf path with a nested path".to_string());
}
let first = paths[0].containers[0];
let is_uniform = matches!(first, NewtypeContainer::Optional | NewtypeContainer::Vec)
&& paths.iter().all(|path| path.containers[0] == first);
let is_map = matches!(first, NewtypeContainer::MapKey | NewtypeContainer::MapValue)
&& paths.iter().all(|path| {
matches!(
path.containers[0],
NewtypeContainer::MapKey | NewtypeContainer::MapValue
)
});
if !is_uniform && !is_map {
return Err("transparent newtype metadata contains incompatible container paths".to_string());
}
if is_uniform {
let nested: Vec<_> = paths
.iter()
.map(|path| NewtypeWrapperMetadata {
rust_path: path.rust_path.clone(),
conversion: path.conversion.clone(),
containers: path.containers[1..].to_vec(),
})
.collect();
return validate_newtype_path_shape(&nested);
}
for branch in [NewtypeContainer::MapKey, NewtypeContainer::MapValue] {
let nested: Vec<_> = paths
.iter()
.filter(|path| path.containers[0] == branch)
.map(|path| NewtypeWrapperMetadata {
rust_path: path.rust_path.clone(),
conversion: path.conversion.clone(),
containers: path.containers[1..].to_vec(),
})
.collect();
if !nested.is_empty() {
validate_newtype_path_shape(&nested)?;
}
}
Ok(())
}
fn validate_newtype_path(
ty: &TypeRef,
mut outer_optional: bool,
containers: &[NewtypeContainer],
) -> Result<(), String> {
let mut current = ty;
for container in containers {
if *container == NewtypeContainer::Optional && outer_optional {
outer_optional = false;
continue;
}
current = match (container, current) {
(NewtypeContainer::Optional, TypeRef::Optional(inner)) | (NewtypeContainer::Vec, TypeRef::Vec(inner)) => {
inner
}
(NewtypeContainer::MapKey, TypeRef::Map(key, _)) => key,
(NewtypeContainer::MapValue, TypeRef::Map(_, value)) => value,
(container, ty) => {
return Err(format!(
"transparent newtype path segment {} cannot traverse {}",
newtype_container_name(*container),
type_ref_name(ty)
));
}
};
}
if outer_optional {
return Err("transparent newtype path does not traverse the outer optional value".to_string());
}
if current != &TypeRef::String {
return Err(format!(
"transparent newtype path must terminate at String, not {}",
type_ref_name(current)
));
}
Ok(())
}
fn newtype_container_name(container: NewtypeContainer) -> &'static str {
match container {
NewtypeContainer::Optional => "optional",
NewtypeContainer::Vec => "vec",
NewtypeContainer::MapKey => "map_key",
NewtypeContainer::MapValue => "map_value",
}
}
fn type_ref_name(ty: &TypeRef) -> &'static str {
match ty {
TypeRef::String => "String",
TypeRef::Optional(_) => "Option",
TypeRef::Vec(_) => "Vec",
TypeRef::Map(_, _) => "Map",
_ => "non-string type",
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub enum CoreWrapper {
#[default]
None,
Cow,
Arc,
Bytes,
ArcMutex,
Box,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum DefaultValue {
BoolLiteral(bool),
StringLiteral(String),
IntLiteral(i64),
FloatLiteral(f64),
EnumVariant(String),
TupleVariant(String, Vec<DefaultValue>),
StructVariant(String, Vec<(String, DefaultValue)>),
FunctionCall(String),
PublicFunctionCall(String),
ListLiteral(Vec<DefaultValue>),
Empty,
Unresolved(String),
None,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct ErrorTaxonomy {
#[serde(default)]
pub code: u32,
#[serde(default)]
pub error_type: String,
#[serde(default)]
pub variant: String,
}
impl ErrorTaxonomy {
pub fn for_variant(code: u32, error_type: &str, variant: &str) -> Self {
Self {
code,
error_type: error_type.to_string(),
variant: variant.to_string(),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct DeprecationInfo {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub since: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct VersionAnnotation {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub legacy_constructor_fields: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub since: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deprecated: Option<DeprecationInfo>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
pub struct SerdeContainerConversion {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub from: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub into: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub try_from: Option<String>,
#[serde(default)]
pub transparent: bool,
}
impl SerdeContainerConversion {
pub fn is_present(&self) -> bool {
self.from.is_some() || self.into.is_some() || self.try_from.is_some() || self.transparent
}
}
#[cfg(test)]
mod metadata_tests {
use super::{ErrorTaxonomy, NewtypeContainer, NewtypeConversion, NewtypeWrapper, NewtypeWrapperMetadata, TypeRef};
#[test]
fn explicit_variant_code_is_preserved() {
let taxonomy = ErrorTaxonomy::for_variant(101, "sample::RequestError", "InvalidInput");
assert_eq!(taxonomy.code, 101);
assert_eq!(taxonomy.error_type, "sample::RequestError");
assert_eq!(taxonomy.variant, "InvalidInput");
}
#[test]
fn legacy_serialized_taxonomy_defaults_compatibly() {
let taxonomy: ErrorTaxonomy = serde_json::from_str("{}").expect("legacy metadata deserializes");
assert_eq!(taxonomy, ErrorTaxonomy::default());
}
#[test]
fn tuple_newtype_wrapper_preserves_legacy_string_wire_format() {
let wrapper = NewtypeWrapper::decode("sample::Index").expect("legacy metadata decodes");
assert_eq!(wrapper.tuple_path(), Some("sample::Index"));
assert!(wrapper.explicit_paths().is_empty());
}
#[test]
fn transparent_string_wrapper_round_trips_conversion_metadata() {
let metadata = NewtypeWrapperMetadata::transparent_string(
"sample::SecretString",
"from",
"into_inner",
vec![NewtypeContainer::Optional, NewtypeContainer::MapValue],
);
let encoded = NewtypeWrapper::encode_explicit(std::slice::from_ref(&metadata));
let decoded = NewtypeWrapper::decode(&encoded).expect("versioned metadata decodes");
assert!(encoded.starts_with("alef:newtype-conversions:v1:"));
assert_eq!(decoded.explicit_paths(), &[metadata]);
assert_eq!(
&decoded.explicit_paths()[0].conversion,
&NewtypeConversion::TransparentString {
from: "from".to_string(),
into: "into_inner".to_string(),
wrapper_is_clone: None,
}
);
}
#[test]
fn malformed_versioned_newtype_metadata_is_rejected() {
let error = NewtypeWrapper::decode("alef:newtype-conversions:v1:not-json")
.expect_err("malformed metadata must not become a legacy tuple path");
assert!(error.contains("invalid transparent newtype metadata"), "{error}");
}
#[test]
fn empty_and_duplicate_newtype_paths_are_rejected() {
let empty = "alef:newtype-conversions:v1:[]";
assert!(
NewtypeWrapper::decode(empty)
.expect_err("empty path set")
.contains("at least one")
);
let metadata = NewtypeWrapperMetadata::transparent_string("sample::Secret", "from", "into_inner", vec![]);
let duplicate_json = serde_json::to_string(&vec![metadata.clone(), metadata]).expect("metadata serializes");
let duplicate = format!("alef:newtype-conversions:v1:{duplicate_json}");
assert!(
NewtypeWrapper::decode(&duplicate)
.expect_err("duplicate paths")
.contains("duplicate container path")
);
}
#[test]
fn impossible_newtype_path_is_rejected_for_resolved_type() {
let metadata = NewtypeWrapperMetadata::transparent_string(
"sample::Secret",
"from",
"into_inner",
vec![NewtypeContainer::MapKey],
);
let wrapper = NewtypeWrapper::decode(&NewtypeWrapper::encode_explicit(&[metadata])).expect("metadata decodes");
let error = wrapper
.validate_for_type(&TypeRef::String, false)
.expect_err("map path cannot address a string");
assert!(error.contains("map_key cannot traverse String"), "{error}");
}
}