use pretty::BoxDoc;
use crate::import::ImportRef;
use crate::lang::CodeLang;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(bound = "")]
pub enum TypeName<L: CodeLang> {
Importable {
module: String,
name: String,
is_type_only: bool,
alias: Option<String>,
},
Primitive(String),
Array(Box<TypeName<L>>),
Generic {
base: Box<TypeName<L>>,
params: Vec<TypeName<L>>,
},
Union(Vec<TypeName<L>>),
Intersection(Vec<TypeName<L>>),
Pointer(Box<TypeName<L>>),
Slice(Box<TypeName<L>>),
Map {
key: Box<TypeName<L>>,
value: Box<TypeName<L>>,
},
Optional(Box<TypeName<L>>),
Function {
params: Vec<TypeName<L>>,
return_type: Box<TypeName<L>>,
},
Raw(String),
#[doc(hidden)]
_Phantom(std::marker::PhantomData<L>),
}
impl<L: CodeLang> TypeName<L> {
pub fn importable(module: &str, name: &str) -> Self {
TypeName::Importable {
module: module.to_string(),
name: name.to_string(),
is_type_only: false,
alias: None,
}
}
pub fn importable_type(module: &str, name: &str) -> Self {
TypeName::Importable {
module: module.to_string(),
name: name.to_string(),
is_type_only: true,
alias: None,
}
}
pub fn primitive(name: &str) -> Self {
TypeName::Primitive(name.to_string())
}
pub fn is_empty(&self) -> bool {
matches!(self, TypeName::Primitive(s) | TypeName::Raw(s) if s.is_empty())
}
pub fn with_alias(mut self, alias: &str) -> Self {
if let TypeName::Importable {
alias: ref mut a, ..
} = self
{
*a = Some(alias.to_string());
}
self
}
pub fn array(inner: TypeName<L>) -> Self {
TypeName::Array(Box::new(inner))
}
pub fn generic(base: TypeName<L>, params: Vec<TypeName<L>>) -> Self {
TypeName::Generic {
base: Box::new(base),
params,
}
}
pub fn union(members: Vec<TypeName<L>>) -> Self {
TypeName::Union(members)
}
pub fn intersection(members: Vec<TypeName<L>>) -> Self {
TypeName::Intersection(members)
}
pub fn pointer(inner: TypeName<L>) -> Self {
TypeName::Pointer(Box::new(inner))
}
pub fn slice(inner: TypeName<L>) -> Self {
TypeName::Slice(Box::new(inner))
}
pub fn map(key: TypeName<L>, value: TypeName<L>) -> Self {
TypeName::Map {
key: Box::new(key),
value: Box::new(value),
}
}
pub fn optional(inner: TypeName<L>) -> Self {
TypeName::Optional(Box::new(inner))
}
pub fn function(params: Vec<TypeName<L>>, return_type: TypeName<L>) -> Self {
TypeName::Function {
params,
return_type: Box::new(return_type),
}
}
pub fn raw(s: &str) -> Self {
TypeName::Raw(s.to_string())
}
pub fn simple_name(&self) -> Option<&str> {
match self {
TypeName::Importable { name, .. } => Some(name),
TypeName::Primitive(name) => Some(name),
TypeName::Generic { base, .. } => base.simple_name(),
TypeName::Raw(s) => Some(s),
_ => None,
}
}
pub fn collect_imports(&self, out: &mut Vec<ImportRef>) {
match self {
TypeName::Importable {
module,
name,
is_type_only,
alias,
} => {
out.push(ImportRef {
module: module.clone(),
name: name.clone(),
is_type_only: *is_type_only,
alias: alias.clone(),
});
}
TypeName::Array(inner)
| TypeName::Pointer(inner)
| TypeName::Slice(inner)
| TypeName::Optional(inner) => {
inner.collect_imports(out);
}
TypeName::Generic { base, params } => {
base.collect_imports(out);
for p in params {
p.collect_imports(out);
}
}
TypeName::Union(members) | TypeName::Intersection(members) => {
for m in members {
m.collect_imports(out);
}
}
TypeName::Map { key, value } => {
key.collect_imports(out);
value.collect_imports(out);
}
TypeName::Function {
params,
return_type,
} => {
for p in params {
p.collect_imports(out);
}
return_type.collect_imports(out);
}
TypeName::Primitive(_) | TypeName::Raw(_) | TypeName::_Phantom(_) => {}
}
}
pub fn to_doc<F>(&self, resolve: &F) -> BoxDoc<'static, ()>
where
F: Fn(&str, &str) -> String,
{
match self {
TypeName::Importable { module, name, .. } => {
let display = resolve(module, name);
BoxDoc::text(display)
}
TypeName::Primitive(name) => BoxDoc::text(name.clone()),
TypeName::Raw(s) => BoxDoc::text(s.clone()),
TypeName::Array(inner) => {
inner.to_doc(resolve).append(BoxDoc::text("[]"))
}
TypeName::Generic { base, params } => {
let base_doc = base.to_doc(resolve);
let params_docs: Vec<_> = params.iter().map(|p| p.to_doc(resolve)).collect();
let sep = BoxDoc::text(",").append(BoxDoc::softline());
let params_doc = BoxDoc::intersperse(params_docs, sep);
base_doc
.append(BoxDoc::text("<"))
.append(params_doc.nest(2).group())
.append(BoxDoc::text(">"))
}
TypeName::Union(members) => {
let docs: Vec<_> = members.iter().map(|m| m.to_doc(resolve)).collect();
let sep = BoxDoc::softline().append(BoxDoc::text("| "));
BoxDoc::intersperse(docs, sep).group()
}
TypeName::Intersection(members) => {
let docs: Vec<_> = members.iter().map(|m| m.to_doc(resolve)).collect();
let sep = BoxDoc::softline().append(BoxDoc::text("& "));
BoxDoc::intersperse(docs, sep).group()
}
TypeName::Pointer(inner) => BoxDoc::text("*").append(inner.to_doc(resolve)),
TypeName::Slice(inner) => BoxDoc::text("[]").append(inner.to_doc(resolve)),
TypeName::Map { key, value } => BoxDoc::text("map[")
.append(key.to_doc(resolve))
.append(BoxDoc::text("]"))
.append(value.to_doc(resolve)),
TypeName::Optional(inner) => {
let inner_doc = inner.to_doc(resolve);
inner_doc
.append(BoxDoc::softline())
.append(BoxDoc::text("| null"))
.group()
}
TypeName::Function {
params,
return_type,
} => {
let params_docs: Vec<_> = params.iter().map(|p| p.to_doc(resolve)).collect();
let sep = BoxDoc::text(",").append(BoxDoc::softline());
let params_doc = BoxDoc::intersperse(params_docs, sep);
BoxDoc::text("(")
.append(params_doc.nest(2).group())
.append(BoxDoc::text(") => "))
.append(return_type.to_doc(resolve))
}
TypeName::_Phantom(_) => BoxDoc::nil(),
}
}
pub fn render<F>(
&self,
width: usize,
resolve: &F,
) -> Result<String, crate::error::SigilStitchError>
where
F: Fn(&str, &str) -> String,
{
let doc = self.to_doc(resolve);
let mut buf = Vec::new();
doc.render(width, &mut buf)
.map_err(|e| crate::error::SigilStitchError::Render {
context: "TypeName::render".to_string(),
message: e.to_string(),
})?;
String::from_utf8(buf).map_err(|e| crate::error::SigilStitchError::Render {
context: "TypeName::render UTF-8 conversion".to_string(),
message: e.to_string(),
})
}
pub fn to_doc_with_lang<F>(&self, resolve: &F, lang: &L) -> BoxDoc<'static, ()>
where
F: Fn(&str, &str) -> String,
{
match self {
TypeName::Generic { base, params } => {
let base_doc = base.to_doc_with_lang(resolve, lang);
let params_docs: Vec<_> = params
.iter()
.map(|p| p.to_doc_with_lang(resolve, lang))
.collect();
let sep = BoxDoc::text(",").append(BoxDoc::softline());
let params_doc = BoxDoc::intersperse(params_docs, sep);
base_doc
.append(BoxDoc::text(lang.generic_open().to_string()))
.append(params_doc.nest(2).group())
.append(BoxDoc::text(lang.generic_close().to_string()))
}
TypeName::Array(inner) => inner
.to_doc_with_lang(resolve, lang)
.append(BoxDoc::text("[]")),
TypeName::Union(members) => {
let docs: Vec<_> = members
.iter()
.map(|m| m.to_doc_with_lang(resolve, lang))
.collect();
let sep = BoxDoc::softline().append(BoxDoc::text("| "));
BoxDoc::intersperse(docs, sep).group()
}
TypeName::Intersection(members) => {
let docs: Vec<_> = members
.iter()
.map(|m| m.to_doc_with_lang(resolve, lang))
.collect();
let sep = BoxDoc::softline().append(BoxDoc::text("& "));
BoxDoc::intersperse(docs, sep).group()
}
TypeName::Pointer(inner) => {
BoxDoc::text("*").append(inner.to_doc_with_lang(resolve, lang))
}
TypeName::Slice(inner) => {
BoxDoc::text("[]").append(inner.to_doc_with_lang(resolve, lang))
}
TypeName::Map { key, value } => BoxDoc::text("map[")
.append(key.to_doc_with_lang(resolve, lang))
.append(BoxDoc::text("]"))
.append(value.to_doc_with_lang(resolve, lang)),
TypeName::Optional(inner) => {
let inner_doc = inner.to_doc_with_lang(resolve, lang);
inner_doc
.append(BoxDoc::softline())
.append(BoxDoc::text("| null"))
.group()
}
TypeName::Function {
params,
return_type,
} => {
let params_docs: Vec<_> = params
.iter()
.map(|p| p.to_doc_with_lang(resolve, lang))
.collect();
let sep = BoxDoc::text(",").append(BoxDoc::softline());
let params_doc = BoxDoc::intersperse(params_docs, sep);
BoxDoc::text("(")
.append(params_doc.nest(2).group())
.append(BoxDoc::text(") => "))
.append(return_type.to_doc_with_lang(resolve, lang))
}
_ => self.to_doc(resolve),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lang::typescript::TypeScript;
fn identity_resolve(module: &str, name: &str) -> String {
let _ = module;
name.to_string()
}
#[test]
fn test_primitive() {
let t = TypeName::<TypeScript>::primitive("number");
assert_eq!(t.render(80, &identity_resolve).unwrap(), "number");
}
#[test]
fn test_importable() {
let t = TypeName::<TypeScript>::importable("./models", "User");
assert_eq!(t.render(80, &identity_resolve).unwrap(), "User");
}
#[test]
fn test_importable_with_alias() {
let t = TypeName::<TypeScript>::importable("./other", "User");
let resolve = |module: &str, name: &str| {
if module == "./other" && name == "User" {
"OtherUser".to_string()
} else {
name.to_string()
}
};
assert_eq!(t.render(80, &resolve).unwrap(), "OtherUser");
}
#[test]
fn test_array() {
let t = TypeName::<TypeScript>::array(TypeName::primitive("string"));
assert_eq!(t.render(80, &identity_resolve).unwrap(), "string[]");
}
#[test]
fn test_generic() {
let t = TypeName::<TypeScript>::generic(
TypeName::primitive("Promise"),
vec![TypeName::importable("./models", "User")],
);
assert_eq!(t.render(80, &identity_resolve).unwrap(), "Promise<User>");
}
#[test]
fn test_generic_multiline() {
let t = TypeName::<TypeScript>::generic(
TypeName::primitive("Map"),
vec![
TypeName::primitive("VeryLongKeyTypeName"),
TypeName::primitive("VeryLongValueTypeName"),
],
);
let output = t.render(20, &identity_resolve).unwrap();
assert!(output.contains('\n'));
assert!(output.contains("VeryLongKeyTypeName"));
assert!(output.contains("VeryLongValueTypeName"));
}
#[test]
fn test_union() {
let t = TypeName::<TypeScript>::union(vec![
TypeName::primitive("string"),
TypeName::primitive("number"),
TypeName::primitive("boolean"),
]);
assert_eq!(
t.render(80, &identity_resolve).unwrap(),
"string | number | boolean"
);
}
#[test]
fn test_union_multiline() {
let t = TypeName::<TypeScript>::union(vec![
TypeName::primitive("VeryLongTypeName1"),
TypeName::primitive("VeryLongTypeName2"),
TypeName::primitive("VeryLongTypeName3"),
]);
let output = t.render(30, &identity_resolve).unwrap();
assert!(output.contains('\n'));
}
#[test]
fn test_pointer() {
let t = TypeName::<TypeScript>::pointer(TypeName::primitive("User"));
assert_eq!(t.render(80, &identity_resolve).unwrap(), "*User");
}
#[test]
fn test_slice() {
let t = TypeName::<TypeScript>::slice(TypeName::primitive("User"));
assert_eq!(t.render(80, &identity_resolve).unwrap(), "[]User");
}
#[test]
fn test_map() {
let t =
TypeName::<TypeScript>::map(TypeName::primitive("string"), TypeName::primitive("User"));
assert_eq!(t.render(80, &identity_resolve).unwrap(), "map[string]User");
}
#[test]
fn test_optional() {
let t = TypeName::<TypeScript>::optional(TypeName::primitive("string"));
assert_eq!(t.render(80, &identity_resolve).unwrap(), "string | null");
}
#[test]
fn test_function_type() {
let t = TypeName::<TypeScript>::function(
vec![TypeName::primitive("string"), TypeName::primitive("number")],
TypeName::primitive("boolean"),
);
assert_eq!(
t.render(80, &identity_resolve).unwrap(),
"(string, number) => boolean"
);
}
#[test]
fn test_deeply_nested() {
let inner = TypeName::<TypeScript>::generic(
TypeName::primitive("Array"),
vec![TypeName::importable("./models", "User")],
);
let outer = TypeName::generic(TypeName::primitive("Promise"), vec![inner]);
assert_eq!(
outer.render(80, &identity_resolve).unwrap(),
"Promise<Array<User>>"
);
}
#[test]
fn test_collect_imports() {
let t = TypeName::<TypeScript>::generic(
TypeName::importable("./base", "Base"),
vec![
TypeName::importable("./models", "User"),
TypeName::array(TypeName::importable("./models", "Tag")),
],
);
let mut imports = Vec::new();
t.collect_imports(&mut imports);
assert_eq!(imports.len(), 3);
assert_eq!(imports[0].name, "Base");
assert_eq!(imports[1].name, "User");
assert_eq!(imports[2].name, "Tag");
}
#[test]
fn test_raw_no_imports() {
let t = TypeName::<TypeScript>::raw("any");
let mut imports = Vec::new();
t.collect_imports(&mut imports);
assert!(imports.is_empty());
assert_eq!(t.render(80, &identity_resolve).unwrap(), "any");
}
#[test]
fn test_with_alias_on_importable() {
let t = TypeName::<TypeScript>::importable("./models", "User").with_alias("MyUser");
if let TypeName::Importable { alias, .. } = &t {
assert_eq!(alias.as_deref(), Some("MyUser"));
} else {
panic!("Expected Importable variant");
}
}
#[test]
fn test_with_alias_propagates_to_import_ref() {
let t = TypeName::<TypeScript>::importable("./models", "User").with_alias("MyUser");
let mut imports = Vec::new();
t.collect_imports(&mut imports);
assert_eq!(imports.len(), 1);
assert_eq!(imports[0].name, "User");
assert_eq!(imports[0].alias.as_deref(), Some("MyUser"));
}
#[test]
fn test_with_alias_noop_on_primitive() {
let t = TypeName::<TypeScript>::primitive("number").with_alias("MyNumber");
assert_eq!(t.render(80, &identity_resolve).unwrap(), "number");
}
#[test]
fn test_with_alias_renders_alias_name() {
let t = TypeName::<TypeScript>::importable("./models", "User").with_alias("MyUser");
let resolve = |_module: &str, _name: &str| "MyUser".to_string();
assert_eq!(t.render(80, &resolve).unwrap(), "MyUser");
}
}