use crate::import::ImportGroup;
use crate::lang::config::{
BlockSyntaxConfig, EnumAndAnnotationConfig, FunctionSyntaxConfig, TypeDeclSyntaxConfig,
TypePresentationConfig,
};
use crate::lang::{CodeLang, RendererLang};
use crate::spec::modifiers::{DeclarationContext, TypeKind, Visibility};
#[derive(Debug, Clone)]
pub struct Ruby {
pub indent: String,
pub extension: String,
}
impl Default for Ruby {
fn default() -> Self {
Self {
indent: " ".to_string(),
extension: "rb".to_string(),
}
}
}
impl Ruby {
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
}
}
#[rustfmt::skip]
const RUBY_RESERVED: &[&str] = &[
"__ENCODING__", "__LINE__", "__FILE__", "BEGIN", "END",
"alias", "and", "begin", "break", "case", "class", "def",
"defined?", "do", "else", "elsif", "end", "ensure", "false",
"for", "if", "in", "module", "next", "nil", "not", "or",
"redo", "rescue", "retry", "return", "self", "super", "then",
"true", "undef", "unless", "until", "when", "while", "yield",
];
impl RendererLang for Ruby {
fn file_extension(&self) -> &str {
&self.extension
}
fn line_comment_prefix(&self) -> &str {
"#"
}
fn render_string_literal(&self, s: &str) -> String {
let escaped = s
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('#', "\\#")
.replace('\n', "\\n")
.replace('\t', "\\t")
.replace('\r', "\\r");
format!("\"{escaped}\"")
}
fn render_verbatim_string(&self, s: &str) -> String {
let escaped = s.replace('\\', "\\\\").replace('\'', "\\'");
format!("'{escaped}'")
}
fn reserved_words(&self) -> &[&str] {
RUBY_RESERVED
}
fn module_separator(&self) -> Option<&str> {
Some("::")
}
fn block_syntax(&self) -> BlockSyntaxConfig<'_> {
BlockSyntaxConfig {
block_open: "",
block_close: "end",
close_on_transition: false,
indent_unit: &self.indent,
uses_semicolons: false,
field_terminator: "",
..Default::default()
}
}
fn type_presentation(&self) -> TypePresentationConfig<'_> {
TypePresentationConfig {
optional_absent_literal: "nil",
..Default::default()
}
}
}
impl CodeLang for Ruby {
fn render_visibility(&self, vis: Visibility, ctx: DeclarationContext) -> &str {
match ctx {
DeclarationContext::TopLevel => "",
DeclarationContext::Member => match vis {
Visibility::Public => "public\n",
Visibility::Private => "private\n",
Visibility::Protected => "protected\n",
_ => "public\n",
},
DeclarationContext::InterfaceMember => "",
}
}
fn function_keyword(&self, _ctx: DeclarationContext) -> &str {
"def"
}
fn type_keyword(&self, kind: TypeKind) -> &str {
match kind {
TypeKind::Class | TypeKind::Struct => "class",
TypeKind::Trait | TypeKind::Interface => "module",
TypeKind::Enum => "class",
TypeKind::TypeAlias | TypeKind::Newtype => "class",
}
}
fn methods_inside_type_body(&self, _kind: TypeKind) -> bool {
true
}
fn render_imports(&self, imports: &ImportGroup) -> String {
let mut lines = Vec::new();
for entry in imports.entries() {
if entry.is_side_effect || entry.name.is_empty() || entry.is_wildcard {
lines.push(format!("require '{}'", entry.module));
} else {
let module_path = entry.module.replace("::", "/");
let name = entry.resolved_name();
if entry.module.is_empty() {
lines.push(format!("require '{}'", name));
} else {
lines.push(format!("require '{module_path}'",));
}
}
}
lines.sort();
lines.dedup();
lines.join("\n")
}
fn render_doc_comment(&self, lines: &[&str]) -> String {
let mut out = String::new();
for line in lines {
if line.is_empty() {
out.push_str("#\n");
} else {
out.push_str(&format!("# {line}\n"));
}
}
out
}
fn fun_block_open(&self) -> &str {
"" }
fn type_header_block_open(&self, _kind: TypeKind) -> &str {
"" }
fn property_style(&self) -> crate::spec::modifiers::PropertyStyle {
crate::spec::modifiers::PropertyStyle::Field
}
fn property_getter_keyword(&self) -> &str {
"attr_reader"
}
fn type_decl_syntax(&self) -> TypeDeclSyntaxConfig<'_> {
TypeDeclSyntaxConfig {
type_before_name: false,
return_type_is_prefix: false,
type_annotation_separator: "", super_type_keyword: " < ",
..Default::default()
}
}
fn function_syntax(&self) -> FunctionSyntaxConfig<'_> {
FunctionSyntaxConfig {
return_type_separator: "", empty_body: "",
static_keyword: "self.",
..Default::default()
}
}
fn enum_and_annotation(&self) -> EnumAndAnnotationConfig<'_> {
EnumAndAnnotationConfig {
readonly_keyword: "",
annotation_prefix: "# ",
annotation_suffix: "",
..Default::default()
}
}
}
#[cfg(test)]
mod tests {
use crate::import::ImportEntry;
use super::*;
#[test]
fn test_file_extension() {
let ruby = Ruby::new();
assert_eq!(ruby.file_extension(), "rb");
}
#[test]
fn test_line_comment() {
let ruby = Ruby::new();
assert_eq!(ruby.line_comment_prefix(), "#");
}
#[test]
fn test_render_string_literal() {
let ruby = Ruby::new();
assert_eq!(ruby.render_string_literal("hello"), "\"hello\"");
assert_eq!(
ruby.render_string_literal("say \"hi\""),
"\"say \\\"hi\\\"\""
);
assert_eq!(
ruby.render_string_literal("interp #{x}"),
"\"interp \\#{x}\""
);
assert_eq!(
ruby.render_string_literal("line1\nline2"),
"\"line1\\nline2\""
);
}
#[test]
fn test_render_verbatim_string() {
let ruby = Ruby::new();
let out = ruby.render_verbatim_string("Hello #{name}");
assert_eq!(out, "'Hello #{name}'");
}
#[test]
fn test_escape_reserved() {
let ruby = Ruby::new();
assert_eq!(ruby.escape_reserved("foo"), "foo");
assert_eq!(ruby.escape_reserved("class"), "class_");
assert_eq!(ruby.escape_reserved("end"), "end_");
assert_eq!(ruby.escape_reserved("def"), "def_");
}
#[test]
fn test_render_imports() {
let ruby = Ruby::new();
let entries = vec![
ImportEntry {
module: "json".to_string(),
name: "JSON".to_string(),
alias: None,
is_type_only: false,
is_side_effect: false,
is_wildcard: false,
},
ImportEntry {
module: "net/http".to_string(),
name: String::new(),
alias: None,
is_type_only: false,
is_side_effect: true,
is_wildcard: false,
},
];
let group = ImportGroup::from(entries);
let out = ruby.render_imports(&group);
assert!(out.contains("require 'json'"));
assert!(out.contains("require 'net/http'"));
}
#[test]
fn test_render_doc_comment() {
let ruby = Ruby::new();
let out = ruby.render_doc_comment(&["Says hello.", "", "@param name [String]"]);
assert!(out.contains("# Says hello.\n"));
assert!(out.contains("#\n"));
assert!(out.contains("# @param name [String]\n"));
}
#[test]
fn test_block_syntax() {
let ruby = Ruby::new();
let bs = ruby.block_syntax();
assert_eq!(bs.block_open, "");
assert_eq!(bs.block_close, "end");
assert!(!bs.uses_semicolons);
assert!(!bs.close_on_transition);
}
#[test]
fn test_visibility() {
let ruby = Ruby::new();
assert_eq!(
ruby.render_visibility(Visibility::Public, DeclarationContext::Member),
"public\n"
);
assert_eq!(
ruby.render_visibility(Visibility::Private, DeclarationContext::Member),
"private\n"
);
assert_eq!(
ruby.render_visibility(Visibility::Public, DeclarationContext::TopLevel),
""
);
}
#[test]
fn test_function_keyword() {
let ruby = Ruby::new();
assert_eq!(ruby.function_keyword(DeclarationContext::TopLevel), "def");
}
#[test]
fn test_type_keyword() {
let ruby = Ruby::new();
assert_eq!(ruby.type_keyword(TypeKind::Class), "class");
assert_eq!(ruby.type_keyword(TypeKind::Interface), "module");
assert_eq!(ruby.type_keyword(TypeKind::Trait), "module");
}
#[test]
fn test_module_separator() {
let ruby = Ruby::new();
assert_eq!(ruby.module_separator(), Some("::"));
}
#[test]
fn test_static_keyword() {
let ruby = Ruby::new();
assert_eq!(ruby.function_syntax().static_keyword, "self.");
}
#[test]
fn test_builder_fluent() {
let ruby = Ruby::new().with_indent("\t").with_extension("rb");
assert_eq!(ruby.file_extension(), "rb");
assert_eq!(ruby.block_syntax().indent_unit, "\t");
}
}