use crate::import::{ImportEntry, ImportGroup};
use crate::lang::CodeLang;
use crate::lang::config::{
BlockSyntaxConfig, EnumAndAnnotationConfig, FunctionSyntaxConfig, GenericSyntaxConfig,
TypeDeclSyntaxConfig, TypePresentationConfig,
};
use crate::spec::fun_spec::WhereClauseStyle;
use crate::spec::modifiers::{DeclarationContext, TypeKind, Visibility};
use crate::type_name::{FunctionPresentation, TypePresentation, WildcardPresentation};
#[derive(Debug, Clone)]
pub struct RustLang {
pub indent: String,
pub extension: String,
}
impl Default for RustLang {
fn default() -> Self {
Self {
indent: " ".to_string(),
extension: "rs".to_string(),
}
}
}
impl RustLang {
pub fn new() -> Self {
Self::default()
}
pub fn with_indent(mut self, s: &str) -> Self {
self.indent = s.to_string();
self
}
pub fn with_extension(mut self, s: &str) -> Self {
self.extension = s.to_string();
self
}
}
const RUST_RESERVED: &[&str] = &[
"as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern",
"false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub",
"ref", "return", "self", "Self", "static", "struct", "super", "trait", "true", "type",
"unsafe", "use", "where", "while",
"abstract", "become", "box", "do", "final", "gen", "macro", "override", "priv", "try", "typeof",
"unsized", "virtual", "yield",
];
impl CodeLang for RustLang {
fn file_extension(&self) -> &str {
&self.extension
}
fn reserved_words(&self) -> &[&str] {
RUST_RESERVED
}
fn render_imports(&self, imports: &ImportGroup) -> String {
if imports.entries.is_empty() {
return String::new();
}
let mut lines = Vec::new();
for entry in &imports.entries {
if entry.is_wildcard {
lines.push(format!("use {}::*;", entry.module));
} else if entry.is_side_effect {
lines.push(format!("use {};", entry.module));
}
}
let mut std_imports: Vec<&ImportEntry> = Vec::new();
let mut external_imports: Vec<&ImportEntry> = Vec::new();
let mut crate_imports: Vec<&ImportEntry> = Vec::new();
for entry in &imports.entries {
if entry.is_side_effect || entry.is_wildcard {
continue;
}
if entry.module.starts_with("std::")
|| entry.module.starts_with("core::")
|| entry.module == "std"
|| entry.module == "core"
{
std_imports.push(entry);
} else if entry.module.starts_with("crate::")
|| entry.module.starts_with("super::")
|| entry.module.starts_with("self::")
{
crate_imports.push(entry);
} else {
external_imports.push(entry);
}
}
fn render_group(entries: &[&ImportEntry], lines: &mut Vec<String>) {
let mut by_module: std::collections::BTreeMap<&str, Vec<&ImportEntry>> =
std::collections::BTreeMap::new();
for entry in entries {
by_module.entry(&entry.module).or_default().push(entry);
}
for (module, items) in &by_module {
if items.len() == 1 {
let entry = items[0];
if let Some(alias) = &entry.alias {
lines.push(format!("use {module}::{} as {alias};", entry.name));
} else {
lines.push(format!("use {module}::{};", entry.name));
}
} else {
let mut specs: Vec<String> = items
.iter()
.map(|e| {
if let Some(alias) = &e.alias {
format!("{} as {alias}", e.name)
} else {
e.name.clone()
}
})
.collect();
specs.sort();
lines.push(format!("use {module}::{{{}}};", specs.join(", ")));
}
}
}
if !std_imports.is_empty() {
render_group(&std_imports, &mut lines);
}
if !external_imports.is_empty() {
if !lines.is_empty() {
lines.push(String::new());
}
render_group(&external_imports, &mut lines);
}
if !crate_imports.is_empty() {
if !lines.is_empty() {
lines.push(String::new());
}
render_group(&crate_imports, &mut lines);
}
lines.join("\n")
}
fn render_string_literal(&self, s: &str) -> String {
format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\""))
}
fn render_doc_comment(&self, lines: &[&str]) -> String {
lines
.iter()
.map(|line| {
if line.is_empty() {
"///".to_string()
} else {
format!("/// {line}")
}
})
.collect::<Vec<_>>()
.join("\n")
}
fn line_comment_prefix(&self) -> &str {
"//"
}
fn escape_reserved(&self, name: &str) -> String {
if self.reserved_words().contains(&name) {
format!("r#{name}")
} else {
name.to_string()
}
}
fn render_visibility(&self, vis: Visibility, _ctx: DeclarationContext) -> &str {
match vis {
Visibility::Inherited => "",
Visibility::Public => "pub ",
Visibility::PublicCrate => "pub(crate) ",
Visibility::PublicSuper => "pub(super) ",
Visibility::Private | Visibility::Protected => "",
}
}
fn function_keyword(&self, _ctx: DeclarationContext) -> &str {
"fn"
}
fn type_keyword(&self, kind: TypeKind) -> &str {
match kind {
TypeKind::Struct | TypeKind::Class => "struct",
TypeKind::Trait | TypeKind::Interface => "trait",
TypeKind::Enum => "enum",
TypeKind::TypeAlias => "type",
TypeKind::Newtype => "struct",
}
}
fn methods_inside_type_body(&self, kind: TypeKind) -> bool {
match kind {
TypeKind::Trait | TypeKind::Interface => true,
TypeKind::Struct
| TypeKind::Class
| TypeKind::Enum
| TypeKind::TypeAlias
| TypeKind::Newtype => false,
}
}
fn optional_field_style(&self) -> crate::lang::config::OptionalFieldStyle {
crate::lang::config::OptionalFieldStyle::TypeWrap {
open: "Option<",
close: ">",
}
}
fn type_presentation(&self) -> TypePresentationConfig<'_> {
TypePresentationConfig {
array: TypePresentation::GenericWrap { name: "Vec" },
readonly_array: Some(TypePresentation::GenericWrap { name: "Vec" }),
optional: TypePresentation::GenericWrap { name: "Option" },
map: TypePresentation::GenericWrap { name: "HashMap" },
intersection: TypePresentation::Infix { sep: " + " },
pointer: TypePresentation::Prefix { prefix: "*const " },
slice: TypePresentation::Delimited {
open: "&[",
sep: "",
close: "]",
},
reference: TypePresentation::Prefix { prefix: "&" },
reference_mut: TypePresentation::Prefix { prefix: "&mut " },
function: FunctionPresentation {
keyword: "fn",
params_open: "(",
params_sep: ", ",
params_close: ")",
arrow: " -> ",
return_first: false,
curried: false,
wrapper_open: "",
wrapper_close: "",
},
wildcard: WildcardPresentation {
unbounded: "_",
upper_keyword: "impl ",
lower_keyword: "impl ",
},
..Default::default()
}
}
fn generic_syntax(&self) -> GenericSyntaxConfig<'_> {
GenericSyntaxConfig::default()
}
fn block_syntax(&self) -> BlockSyntaxConfig<'_> {
BlockSyntaxConfig {
indent_unit: &self.indent,
..Default::default()
}
}
fn function_syntax(&self) -> FunctionSyntaxConfig<'_> {
FunctionSyntaxConfig {
return_type_separator: " -> ",
constructor_keyword: "fn",
where_clause_style: WhereClauseStyle::WhereBlock,
..Default::default()
}
}
fn type_decl_syntax(&self) -> TypeDeclSyntaxConfig<'_> {
TypeDeclSyntaxConfig::default()
}
fn enum_and_annotation(&self) -> EnumAndAnnotationConfig<'_> {
EnumAndAnnotationConfig {
variant_trailing_separator: true,
annotation_prefix: "#[",
annotation_suffix: "]",
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_file_extension() {
let rs = RustLang::new();
assert_eq!(rs.file_extension(), "rs");
}
#[test]
fn test_escape_reserved() {
let rs = RustLang::new();
assert_eq!(rs.escape_reserved("type"), "r#type");
assert_eq!(rs.escape_reserved("my_var"), "my_var");
assert_eq!(rs.escape_reserved("gen"), "r#gen");
}
#[test]
fn test_render_imports_grouped() {
let rs = RustLang::new();
let imports = ImportGroup {
entries: vec![
ImportEntry {
module: "std::collections".into(),
name: "HashMap".into(),
alias: None,
is_type_only: false,
is_side_effect: false,
is_wildcard: false,
},
ImportEntry {
module: "std::collections".into(),
name: "BTreeMap".into(),
alias: None,
is_type_only: false,
is_side_effect: false,
is_wildcard: false,
},
ImportEntry {
module: "serde".into(),
name: "Serialize".into(),
alias: None,
is_type_only: false,
is_side_effect: false,
is_wildcard: false,
},
ImportEntry {
module: "crate::models".into(),
name: "User".into(),
alias: None,
is_type_only: false,
is_side_effect: false,
is_wildcard: false,
},
],
};
let output = rs.render_imports(&imports);
let lines: Vec<&str> = output.lines().collect();
assert_eq!(lines[0], "use std::collections::{BTreeMap, HashMap};");
assert_eq!(lines[1], "");
assert_eq!(lines[2], "use serde::Serialize;");
assert_eq!(lines[3], "");
assert_eq!(lines[4], "use crate::models::User;");
}
#[test]
fn test_render_imports_with_alias() {
let rs = RustLang::new();
let imports = ImportGroup {
entries: vec![ImportEntry {
module: "models".into(),
name: "User".into(),
alias: Some("ModelsUser".into()),
is_type_only: false,
is_side_effect: false,
is_wildcard: false,
}],
};
let output = rs.render_imports(&imports);
assert_eq!(output, "use models::User as ModelsUser;");
}
#[test]
fn test_doc_comment() {
let rs = RustLang::new();
let doc = rs.render_doc_comment(&["Get the user.", "", "Returns None if not found."]);
assert!(doc.contains("/// Get the user."));
assert!(doc.contains("///\n"));
assert!(doc.contains("/// Returns None if not found."));
}
#[test]
fn test_string_literal() {
let rs = RustLang::new();
assert_eq!(rs.render_string_literal("hello"), "\"hello\"");
assert_eq!(rs.render_string_literal("it\"s"), "\"it\\\"s\"");
}
#[test]
fn test_rust_builder_fluent() {
let rs = RustLang::new().with_indent("\t").with_extension("rsi");
assert_eq!(rs.file_extension(), "rsi");
assert_eq!(rs.block_syntax().indent_unit, "\t");
}
}