use crate::code_block::{Arg, CodeBlock, CodeBlockBuilder};
use crate::lang::CodeLang;
use crate::spec::annotation_spec::AnnotationSpec;
use crate::spec::enum_variant_spec::EnumVariantSpec;
use crate::spec::field_spec::FieldSpec;
use crate::spec::fun_spec::{FunSpec, TypeParamSpec, render_type_params};
use crate::spec::modifiers::{DeclarationContext, Modifiers, TypeKind, Visibility};
use crate::spec::parameter_spec::ParameterSpec;
use crate::spec::property_spec::PropertySpec;
use crate::type_name::TypeName;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(bound = "")]
pub struct TypeSpec<L: CodeLang> {
pub(crate) name: String,
pub(crate) kind: TypeKind,
pub(crate) modifiers: Modifiers,
pub(crate) doc: Vec<String>,
pub(crate) fields: Vec<FieldSpec<L>>,
pub(crate) properties: Vec<PropertySpec<L>>,
pub(crate) methods: Vec<FunSpec<L>>,
pub(crate) type_params: Vec<TypeParamSpec<L>>,
pub(crate) super_types: Vec<TypeName<L>>,
pub(crate) impl_types: Vec<TypeName<L>>,
pub(crate) annotations: Vec<CodeBlock<L>>,
pub(crate) annotation_specs: Vec<AnnotationSpec<L>>,
pub(crate) extra_members: Vec<CodeBlock<L>>,
pub(crate) variants: Vec<EnumVariantSpec<L>>,
pub(crate) primary_constructor: Vec<ParameterSpec<L>>,
}
impl<L: CodeLang> TypeSpec<L> {
pub fn builder(name: &str, kind: TypeKind) -> TypeSpecBuilder<L> {
TypeSpecBuilder {
name: name.to_string(),
kind,
modifiers: Modifiers::default(),
doc: Vec::new(),
fields: Vec::new(),
properties: Vec::new(),
methods: Vec::new(),
type_params: Vec::new(),
super_types: Vec::new(),
impl_types: Vec::new(),
annotations: Vec::new(),
annotation_specs: Vec::new(),
extra_members: Vec::new(),
variants: Vec::new(),
primary_constructor: Vec::new(),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn kind(&self) -> TypeKind {
self.kind
}
pub fn emit(&self, lang: &L) -> Result<Vec<CodeBlock<L>>, crate::error::SigilStitchError> {
if lang.methods_inside_type_body(self.kind) {
Ok(vec![self.emit_inline(lang)?])
} else {
self.emit_split(lang)
}
}
fn emit_inline(&self, lang: &L) -> Result<CodeBlock<L>, crate::error::SigilStitchError> {
let mut cb = CodeBlock::<L>::builder();
self.emit_preamble(&mut cb, lang)?;
self.emit_header(&mut cb, lang)?;
cb.add("%>", ());
if !self.doc.is_empty() && lang.doc_comment_inside_body() {
let doc_lines: Vec<&str> = self.doc.iter().map(|s| s.as_str()).collect();
let doc_str = lang.render_doc_comment(&doc_lines);
cb.add("%L", doc_str);
cb.add_line();
}
for (i, field) in self.fields.iter().enumerate() {
if i > 0 {
}
cb.add_code(field.emit(lang, DeclarationContext::Member)?);
}
if !self.variants.is_empty() {
if !self.fields.is_empty() {
cb.add_line();
}
self.emit_variants(&mut cb, lang)?;
}
let has_body_above = !self.fields.is_empty() || !self.variants.is_empty();
if !self.properties.is_empty() {
if has_body_above {
cb.add_line();
}
for (i, prop) in self.properties.iter().enumerate() {
if i > 0 {
cb.add_line();
}
for block in prop.emit(lang, DeclarationContext::Member)? {
cb.add_code(block);
}
}
}
let has_body_above = has_body_above || !self.properties.is_empty();
if has_body_above && !self.methods.is_empty() {
cb.add_line();
}
for (i, method) in self.methods.iter().enumerate() {
if i > 0 {
cb.add_line();
}
cb.add_code(method.emit(lang, DeclarationContext::Member)?);
}
for extra in &self.extra_members {
cb.add_code(extra.clone());
}
cb.add("%<", ());
let close = lang.block_close();
if !close.is_empty() {
let term = lang.type_close_terminator();
cb.add(&format!("{close}{term}"), ());
cb.add_line();
}
cb.build()
}
fn emit_split(&self, lang: &L) -> Result<Vec<CodeBlock<L>>, crate::error::SigilStitchError> {
let mut blocks = Vec::new();
let mut cb = CodeBlock::<L>::builder();
self.emit_preamble(&mut cb, lang)?;
self.emit_header(&mut cb, lang)?;
cb.add("%>", ());
for field in &self.fields {
cb.add_code(field.emit(lang, DeclarationContext::Member)?);
}
if !self.variants.is_empty() {
if !self.fields.is_empty() {
cb.add_line();
}
self.emit_variants(&mut cb, lang)?;
}
for extra in &self.extra_members {
cb.add_code(extra.clone());
}
cb.add("%<", ());
let close = lang.block_close();
if !close.is_empty() {
let term = lang.type_close_terminator();
cb.add(&format!("{close}{term}"), ());
cb.add_line();
}
blocks.push(cb.build()?);
if !self.methods.is_empty() || !self.properties.is_empty() {
let mut impl_cb = CodeBlock::<L>::builder();
let mut impl_fmt = String::from("impl");
let mut impl_args: Vec<Arg<L>> = Vec::new();
let tp_str = render_type_params(&self.type_params, lang, &mut impl_args);
impl_fmt.push_str(&tp_str);
impl_fmt.push(' ');
impl_fmt.push_str(&self.name);
if !self.type_params.is_empty() {
impl_fmt.push_str(lang.generic_open());
for (i, tp) in self.type_params.iter().enumerate() {
if i > 0 {
impl_fmt.push_str(", ");
}
impl_fmt.push_str(&tp.name);
}
impl_fmt.push_str(lang.generic_close());
}
impl_fmt.push_str(lang.block_open());
impl_cb.add(&impl_fmt, impl_args);
impl_cb.add_line();
impl_cb.add("%>", ());
for (i, prop) in self.properties.iter().enumerate() {
if i > 0 {
impl_cb.add_line();
}
for block in prop.emit(lang, DeclarationContext::Member)? {
impl_cb.add_code(block);
}
}
if !self.properties.is_empty() && !self.methods.is_empty() {
impl_cb.add_line();
}
for (i, method) in self.methods.iter().enumerate() {
if i > 0 {
impl_cb.add_line();
}
impl_cb.add_code(method.emit(lang, DeclarationContext::Member)?);
}
impl_cb.add("%<", ());
let close = lang.block_close();
if !close.is_empty() {
impl_cb.add(close, ());
impl_cb.add_line();
}
blocks.push(impl_cb.build()?);
}
Ok(blocks)
}
fn emit_variants(
&self,
cb: &mut CodeBlockBuilder<L>,
lang: &L,
) -> Result<(), crate::error::SigilStitchError> {
let sep = lang.enum_variant_separator();
let trailing = lang.enum_variant_trailing_separator();
let count = self.variants.len();
let field_term = lang.field_terminator();
for (i, variant) in self.variants.iter().enumerate() {
for spec in &variant.annotation_specs {
cb.add_code(spec.emit(lang)?);
cb.add_line();
}
for ann in &variant.annotations {
cb.add_code(ann.clone());
cb.add_line();
}
if !variant.doc.is_empty() && !lang.doc_comment_inside_body() {
let doc_lines: Vec<&str> = variant.doc.iter().map(|s| s.as_str()).collect();
let doc_str = lang.render_doc_comment(&doc_lines);
cb.add("%L", doc_str);
cb.add_line();
}
let prefix = lang.enum_variant_prefix();
let mut fmt = String::new();
let mut args: Vec<Arg<L>> = Vec::new();
fmt.push_str(prefix);
fmt.push_str(&variant.name);
if !variant.associated_types.is_empty() {
fmt.push('(');
for (j, ty) in variant.associated_types.iter().enumerate() {
if j > 0 {
fmt.push_str(", ");
}
fmt.push_str("%T");
args.push(Arg::TypeName(ty.clone()));
}
fmt.push(')');
}
if !variant.fields.is_empty() {
let is_last = i == count - 1;
let needs_sep = !sep.is_empty() && (!is_last || trailing);
fmt.push_str(" {");
cb.add(&fmt, args);
cb.add_line();
cb.add("%>", ());
for field in &variant.fields {
let vis = lang.render_visibility(
field.modifiers.visibility,
crate::spec::modifiers::DeclarationContext::Member,
);
let mut f_fmt = String::new();
let mut f_args: Vec<Arg<L>> = Vec::new();
f_fmt.push_str(vis);
if lang.type_before_name() {
if !field.field_type.is_empty() {
f_fmt.push_str("%T");
f_args.push(Arg::TypeName(field.field_type.clone()));
f_fmt.push(' ');
}
f_fmt.push_str(&field.name);
} else {
f_fmt.push_str(&field.name);
if !field.field_type.is_empty() {
let type_sep = lang.type_annotation_separator();
f_fmt.push_str(type_sep);
f_fmt.push_str("%T");
f_args.push(Arg::TypeName(field.field_type.clone()));
}
}
f_fmt.push_str(field_term);
cb.add(&f_fmt, f_args);
cb.add_line();
}
cb.add("%<", ());
if needs_sep {
cb.add(&format!("}}{sep}"), ());
} else {
cb.add("}", ());
}
cb.add_line();
continue;
}
if let Some(val) = &variant.value {
fmt.push_str(" = %L");
args.push(Arg::Code(val.clone()));
}
let is_last = i == count - 1;
if !sep.is_empty() && (!is_last || trailing) {
fmt.push_str(sep);
}
cb.add(&fmt, args);
cb.add_line();
}
Ok(())
}
fn emit_preamble(
&self,
cb: &mut CodeBlockBuilder<L>,
lang: &L,
) -> Result<(), crate::error::SigilStitchError> {
for spec in &self.annotation_specs {
cb.add_code(spec.emit(lang)?);
cb.add_line();
}
for ann in &self.annotations {
cb.add_code(ann.clone());
cb.add_line();
}
if !self.doc.is_empty() && !lang.doc_comment_inside_body() {
let doc_lines: Vec<&str> = self.doc.iter().map(|s| s.as_str()).collect();
let doc_str = lang.render_doc_comment(&doc_lines);
cb.add("%L", doc_str);
cb.add_line();
}
Ok(())
}
fn emit_header(
&self,
cb: &mut CodeBlockBuilder<L>,
lang: &L,
) -> Result<(), crate::error::SigilStitchError> {
let vis = lang.render_visibility(self.modifiers.visibility, DeclarationContext::TopLevel);
let kw = lang.type_keyword(self.kind);
let mut fmt = String::new();
let mut args: Vec<Arg<L>> = Vec::new();
fmt.push_str(vis);
if self.modifiers.is_abstract {
fmt.push_str("abstract ");
}
fmt.push_str(kw);
fmt.push(' ');
fmt.push_str(&self.name);
let tp_str = render_type_params(&self.type_params, lang, &mut args);
fmt.push_str(&tp_str);
if !self.primary_constructor.is_empty() && lang.supports_primary_constructor() {
fmt.push('(');
fmt.push_str("%L");
let params_block = self.build_primary_constructor_block(lang)?;
args.push(Arg::Code(params_block));
fmt.push(')');
}
if !self.super_types.is_empty() {
let super_kw = lang.super_type_keyword();
if !super_kw.is_empty() {
fmt.push_str(super_kw);
let sep = lang.super_type_separator();
for (i, st) in self.super_types.iter().enumerate() {
if i > 0 {
fmt.push_str(sep);
}
fmt.push_str("%T");
args.push(Arg::TypeName(st.clone()));
}
}
}
if !self.impl_types.is_empty() {
let impl_kw = lang.implements_keyword();
if !impl_kw.is_empty() {
fmt.push_str(impl_kw);
for (i, it) in self.impl_types.iter().enumerate() {
if i > 0 {
fmt.push_str(", ");
}
fmt.push_str("%T");
args.push(Arg::TypeName(it.clone()));
}
}
}
let suffix = lang.type_kind_suffix(self.kind);
if !suffix.is_empty() {
fmt.push(' ');
fmt.push_str(suffix);
}
if !self.super_types.is_empty() || !self.impl_types.is_empty() {
let bases_close = lang.bases_close();
if !bases_close.is_empty() {
fmt.push_str(bases_close);
}
}
fmt.push_str(lang.block_open());
cb.add(&fmt, args);
cb.add_line();
Ok(())
}
fn build_primary_constructor_block(
&self,
lang: &L,
) -> Result<CodeBlock<L>, crate::error::SigilStitchError> {
let mut pb = CodeBlock::<L>::builder();
for (i, param) in self.primary_constructor.iter().enumerate() {
if i > 0 {
pb.add(",%W", ());
}
param.emit_into(&mut pb, lang);
}
pb.build()
}
}
#[derive(Debug)]
pub struct TypeSpecBuilder<L: CodeLang> {
name: String,
kind: TypeKind,
modifiers: Modifiers,
doc: Vec<String>,
fields: Vec<FieldSpec<L>>,
properties: Vec<PropertySpec<L>>,
methods: Vec<FunSpec<L>>,
type_params: Vec<TypeParamSpec<L>>,
super_types: Vec<TypeName<L>>,
impl_types: Vec<TypeName<L>>,
annotations: Vec<CodeBlock<L>>,
annotation_specs: Vec<AnnotationSpec<L>>,
extra_members: Vec<CodeBlock<L>>,
variants: Vec<EnumVariantSpec<L>>,
primary_constructor: Vec<ParameterSpec<L>>,
}
impl<L: CodeLang> TypeSpecBuilder<L> {
pub fn visibility(&mut self, vis: Visibility) -> &mut Self {
self.modifiers.visibility = vis;
self
}
pub fn is_abstract(&mut self) -> &mut Self {
self.modifiers.is_abstract = true;
self
}
pub fn doc(&mut self, line: &str) -> &mut Self {
self.doc.push(line.to_string());
self
}
pub fn add_field(&mut self, field: FieldSpec<L>) -> &mut Self {
self.fields.push(field);
self
}
pub fn add_property(&mut self, prop: PropertySpec<L>) -> &mut Self {
self.properties.push(prop);
self
}
pub fn add_method(&mut self, method: FunSpec<L>) -> &mut Self {
self.methods.push(method);
self
}
pub fn add_type_param(&mut self, tp: TypeParamSpec<L>) -> &mut Self {
self.type_params.push(tp);
self
}
pub fn extends(&mut self, super_type: TypeName<L>) -> &mut Self {
self.super_types.push(super_type);
self
}
pub fn implements(&mut self, iface: TypeName<L>) -> &mut Self {
self.impl_types.push(iface);
self
}
pub fn annotation(&mut self, ann: CodeBlock<L>) -> &mut Self {
self.annotations.push(ann);
self
}
pub fn annotate(&mut self, spec: AnnotationSpec<L>) -> &mut Self {
self.annotation_specs.push(spec);
self
}
pub fn extra_member(&mut self, block: CodeBlock<L>) -> &mut Self {
self.extra_members.push(block);
self
}
pub fn add_variant(&mut self, variant: EnumVariantSpec<L>) -> &mut Self {
self.variants.push(variant);
self
}
pub fn add_primary_constructor_param(&mut self, param: ParameterSpec<L>) -> &mut Self {
self.primary_constructor.push(param);
self
}
pub fn build(self) -> Result<TypeSpec<L>, crate::error::SigilStitchError> {
snafu::ensure!(
!self.name.is_empty(),
crate::error::EmptyNameSnafu {
builder: "TypeSpecBuilder",
}
);
let mut seen = std::collections::HashSet::new();
for field in &self.fields {
if !seen.insert(field.name()) {
return Err(crate::error::SigilStitchError::DuplicateFieldName {
type_name: self.name.clone(),
field_name: field.name().to_string(),
});
}
}
Ok(TypeSpec {
name: self.name,
kind: self.kind,
modifiers: self.modifiers,
doc: self.doc,
fields: self.fields,
properties: self.properties,
methods: self.methods,
type_params: self.type_params,
super_types: self.super_types,
impl_types: self.impl_types,
annotations: self.annotations,
annotation_specs: self.annotation_specs,
extra_members: self.extra_members,
variants: self.variants,
primary_constructor: self.primary_constructor,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lang::rust_lang::RustLang;
use crate::lang::typescript::TypeScript;
use crate::spec::parameter_spec::ParameterSpec;
fn render_blocks_ts(blocks: &[CodeBlock<TypeScript>]) -> String {
let lang = TypeScript::new();
let imports = crate::import::ImportGroup::new();
let mut output = String::new();
for (i, block) in blocks.iter().enumerate() {
if i > 0 {
output.push('\n');
}
let mut renderer = crate::code_renderer::CodeRenderer::new(&lang, &imports, 80);
output.push_str(&renderer.render(block).unwrap());
}
output
}
fn render_blocks_rs(blocks: &[CodeBlock<RustLang>]) -> String {
let lang = RustLang::new();
let imports = crate::import::ImportGroup::new();
let mut output = String::new();
for (i, block) in blocks.iter().enumerate() {
if i > 0 {
output.push('\n');
}
let mut renderer = crate::code_renderer::CodeRenderer::new(&lang, &imports, 80);
output.push_str(&renderer.render(block).unwrap());
}
output
}
#[test]
fn test_ts_class() {
let mut tb = TypeSpec::<TypeScript>::builder("UserService", TypeKind::Class);
tb.visibility(Visibility::Public);
let mut field_b = FieldSpec::builder("name", TypeName::primitive("string"));
field_b.visibility(Visibility::Private);
tb.add_field(field_b.build().unwrap());
let body = CodeBlock::<TypeScript>::of("return this.name", ()).unwrap();
let mut fb = FunSpec::builder("getName");
fb.returns(TypeName::primitive("string"));
fb.body(body);
tb.add_method(fb.build().unwrap());
let ts = tb.build().unwrap();
let blocks = ts.emit(&TypeScript::new()).unwrap();
let output = render_blocks_ts(&blocks);
assert!(output.contains("export class UserService {"));
assert!(output.contains("private name: string;"));
assert!(output.contains("getName(): string {"));
assert!(output.contains("return this.name"));
}
#[test]
fn test_ts_interface() {
let mut tb = TypeSpec::<TypeScript>::builder("Repository", TypeKind::Interface);
tb.visibility(Visibility::Public);
tb.add_method({
let mut fb = FunSpec::builder("findById");
fb.add_param(ParameterSpec::new("id", TypeName::primitive("string")).unwrap());
fb.returns(TypeName::generic(
TypeName::primitive("Promise"),
vec![TypeName::primitive("Entity")],
));
fb.build().unwrap()
});
let ts = tb.build().unwrap();
let blocks = ts.emit(&TypeScript::new()).unwrap();
let output = render_blocks_ts(&blocks);
assert!(output.contains("export interface Repository {"));
assert!(output.contains("findById(id: string): Promise<Entity>;"));
}
#[test]
fn test_rust_struct_with_impl() {
let mut tb = TypeSpec::<RustLang>::builder("Config", TypeKind::Struct);
tb.visibility(Visibility::Public);
tb.add_field({
let mut fb = FieldSpec::builder("name", TypeName::primitive("String"));
fb.visibility(Visibility::Public);
fb.build().unwrap()
});
let body = CodeBlock::<RustLang>::of("Self { name: name.to_string() }", ()).unwrap();
let mut fb = FunSpec::<RustLang>::builder("new");
fb.visibility(Visibility::Public);
fb.add_param(ParameterSpec::new("name", TypeName::primitive("&str")).unwrap());
fb.returns(TypeName::primitive("Self"));
fb.body(body);
tb.add_method(fb.build().unwrap());
let ts = tb.build().unwrap();
let blocks = ts.emit(&RustLang::new()).unwrap();
let output = render_blocks_rs(&blocks);
assert!(output.contains("pub struct Config {"));
assert!(output.contains("pub name: String,"));
assert!(output.contains("impl Config {"));
assert!(output.contains("pub fn new(name: &str) -> Self {"));
}
#[test]
fn test_ts_class_extends_implements() {
let mut tb = TypeSpec::<TypeScript>::builder("AdminService", TypeKind::Class);
tb.visibility(Visibility::Public);
tb.extends(TypeName::primitive("BaseService"));
tb.implements(TypeName::primitive("Serializable"));
let ts = tb.build().unwrap();
let blocks = ts.emit(&TypeScript::new()).unwrap();
let output = render_blocks_ts(&blocks);
assert!(
output.contains(
"export class AdminService extends BaseService implements Serializable {"
)
);
}
#[test]
fn test_build_empty_name_errors() {
let result = TypeSpec::<TypeScript>::builder("", TypeKind::Class).build();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("'name' must not be empty")
);
}
#[test]
fn test_build_duplicate_field_name_errors() {
let mut tb = TypeSpec::<TypeScript>::builder("MyClass", TypeKind::Class);
tb.add_field(
FieldSpec::builder("name", TypeName::primitive("string"))
.build()
.unwrap(),
);
tb.add_field(
FieldSpec::builder("age", TypeName::primitive("number"))
.build()
.unwrap(),
);
tb.add_field(
FieldSpec::builder("name", TypeName::primitive("string"))
.build()
.unwrap(),
);
let result = tb.build();
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("duplicate field name"));
assert!(err_msg.contains("name"));
assert!(err_msg.contains("MyClass"));
}
#[test]
fn test_build_no_duplicate_fields_ok() {
let mut tb = TypeSpec::<TypeScript>::builder("MyClass", TypeKind::Class);
tb.add_field(
FieldSpec::builder("name", TypeName::primitive("string"))
.build()
.unwrap(),
);
tb.add_field(
FieldSpec::builder("age", TypeName::primitive("number"))
.build()
.unwrap(),
);
let result = tb.build();
assert!(result.is_ok());
}
}