use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct TypePath {
segments: Vec<String>,
}
impl TypePath {
pub fn new(segments: Vec<String>) -> Result<Self, TypePathError> {
if segments.is_empty() {
return Err(TypePathError::Empty);
}
Ok(Self { segments })
}
pub fn segments(&self) -> &[String] {
&self.segments
}
pub fn terminal(&self) -> &str {
self.segments.last().expect("TypePath invariant: non-empty segments").as_str()
}
}
impl std::fmt::Display for TypePath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut first = true;
for segment in &self.segments {
if !first {
f.write_str("::")?;
}
f.write_str(segment)?;
first = false;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TypePathError {
Empty,
}
impl std::fmt::Display for TypePathError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Empty => f.write_str("TypePath must have at least one segment"),
}
}
}
impl std::error::Error for TypePathError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum BigIntBehavior {
#[default]
Number,
BigInt,
String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum QuoteStyle {
#[default]
Single,
Double,
}
impl QuoteStyle {
pub(crate) fn delimiter(self) -> char {
match self {
Self::Single => '\'',
Self::Double => '"',
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RenameAll {
Lowercase,
Uppercase,
PascalCase,
CamelCase,
SnakeCase,
ScreamingSnakeCase,
KebabCase,
ScreamingKebabCase,
}
#[derive(Debug, Clone, Default)]
pub struct EmitConfig {
pub external_types: BTreeMap<String, String>,
pub bigint_behavior: BigIntBehavior,
pub case_default: Option<RenameAll>,
pub strict_unsupported: bool,
pub quote_style: QuoteStyle,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EmitError {
UnsupportedShape {
type_path: TypePath,
reason: String,
},
UnsupportedSerdeAttr {
type_path: TypePath,
attr: String,
},
UnresolvedReference {
name: String,
referenced_by: TypePath,
},
NameCollision {
name: String,
paths: Vec<TypePath>,
},
}
impl std::fmt::Display for EmitError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnsupportedShape { type_path, reason } => {
write!(f, "unsupported shape at `{type_path}`: {reason}")
}
Self::UnsupportedSerdeAttr { type_path, attr } => {
write!(f, "unsupported serde attribute `{attr}` on `{type_path}`")
}
Self::UnresolvedReference { name, referenced_by } => {
write!(f, "unresolved reference `{name}` (from `{referenced_by}`)")
}
Self::NameCollision { name, paths } => {
write!(f, "TS name collision on `{name}` between ")?;
let mut first = true;
for path in paths {
if !first {
write!(f, ", ")?;
}
write!(f, "`{path}`")?;
first = false;
}
Ok(())
}
}
}
}
impl std::error::Error for EmitError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn type_path_rejects_empty() {
let err = TypePath::new(Vec::new()).expect_err("empty path should fail");
assert_eq!(err, TypePathError::Empty);
}
#[test]
fn type_path_accepts_single_segment() {
let path = TypePath::new(vec!["Foo".to_string()]).expect("single segment is valid");
assert_eq!(path.segments(), &["Foo".to_string()]);
assert_eq!(path.terminal(), "Foo");
assert_eq!(path.to_string(), "Foo");
}
#[test]
fn type_path_accepts_multi_segment() {
let path = TypePath::new(vec!["crate".to_string(), "models".to_string(), "Workout".to_string()])
.expect("multi-segment is valid");
assert_eq!(path.terminal(), "Workout");
assert_eq!(path.to_string(), "crate::models::Workout");
}
#[test]
fn bigint_behavior_default_is_number() {
assert_eq!(BigIntBehavior::default(), BigIntBehavior::Number);
}
#[test]
fn emit_config_default_is_empty_and_lax() {
let config = EmitConfig::default();
assert!(config.external_types.is_empty());
assert_eq!(config.bigint_behavior, BigIntBehavior::Number);
assert_eq!(config.case_default, None);
assert!(!config.strict_unsupported);
assert_eq!(config.quote_style, QuoteStyle::Single);
}
#[test]
fn quote_style_default_is_single() {
assert_eq!(QuoteStyle::default(), QuoteStyle::Single);
assert_eq!(QuoteStyle::Single.delimiter(), '\'');
assert_eq!(QuoteStyle::Double.delimiter(), '"');
}
#[test]
fn emit_error_display_renders_reasonably() {
let tp = TypePath::new(vec!["crate".to_string(), "Foo".to_string()]).unwrap();
let err = EmitError::UnsupportedShape { type_path: tp.clone(), reason: "tuple struct".to_string() };
assert_eq!(err.to_string(), "unsupported shape at `crate::Foo`: tuple struct");
let err = EmitError::NameCollision {
name: "Foo".to_string(),
paths: vec![tp.clone(), TypePath::new(vec!["other".to_string(), "Foo".to_string()]).unwrap()],
};
assert_eq!(err.to_string(), "TS name collision on `Foo` between `crate::Foo`, `other::Foo`");
}
}