use crate::code_block::CodeBlock;
use crate::code_renderer::CodeRenderer;
use crate::error::SigilStitchError;
use crate::import::ImportGroup;
use crate::import_collector;
use crate::lang::CodeLang;
use crate::spec::emittable::Emittable;
use crate::spec::fun_spec::FunSpec;
use crate::spec::import_spec::ImportSpec;
use crate::spec::modifiers::DeclarationContext;
use crate::spec::type_spec::TypeSpec;
use crate::type_name::TypeName;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub enum FileMember {
Code(CodeBlock),
RawContent(String),
RawContentWithImports {
content: String,
types: Vec<TypeName>,
},
Type(TypeSpec),
Fun(FunSpec),
#[serde(skip)]
Spec(Box<dyn Emittable>),
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct FileSpec {
filename: String,
header: Option<CodeBlock>,
members: Vec<FileMember>,
explicit_imports: Vec<ImportSpec>,
#[serde(skip)]
lang: Option<Box<dyn CodeLang>>,
}
impl FileSpec {
pub fn builder(filename: &str) -> FileSpecBuilder {
let ext = filename.rsplit('.').next().unwrap_or("");
let lang = crate::lang::lang_from_extension(ext);
FileSpecBuilder {
filename: filename.to_string(),
header: None,
members: Vec::new(),
explicit_imports: Vec::new(),
lang,
}
}
pub fn builder_with(filename: &str, lang: impl CodeLang) -> FileSpecBuilder {
FileSpecBuilder {
filename: filename.to_string(),
header: None,
members: Vec::new(),
explicit_imports: Vec::new(),
lang: Some(Box::new(lang)),
}
}
pub fn filename(&self) -> &str {
&self.filename
}
pub fn with_lang(mut self, lang: impl CodeLang) -> Self {
self.lang = Some(Box::new(lang));
self
}
pub fn render(&self, width: usize) -> Result<String, SigilStitchError> {
let lang: &dyn CodeLang =
self.lang
.as_deref()
.ok_or_else(|| SigilStitchError::MissingLang {
filename: self.filename.clone(),
})?;
enum Materialized {
Blocks(Vec<CodeBlock>),
Raw(String),
RawWithImports {
content: String,
types: Vec<TypeName>,
},
}
let mut materialized: Vec<Materialized> = Vec::with_capacity(self.members.len());
for m in &self.members {
materialized.push(match m {
FileMember::Code(b) => Materialized::Blocks(vec![b.clone()]),
FileMember::RawContent(s) => Materialized::Raw(s.clone()),
FileMember::RawContentWithImports { content, types } => {
Materialized::RawWithImports {
content: content.clone(),
types: types.clone(),
}
}
FileMember::Type(spec) => Materialized::Blocks(spec.emit(lang)?),
FileMember::Fun(spec) => {
Materialized::Blocks(vec![spec.emit(lang, DeclarationContext::TopLevel)?])
}
FileMember::Spec(spec) => Materialized::Blocks(spec.emit_members(lang)?),
});
}
let mut import_refs = Vec::new();
if let Some(header) = &self.header {
import_refs.extend(import_collector::collect_imports(header));
}
for mat in &materialized {
match mat {
Materialized::Blocks(blocks) => {
for block in blocks {
import_refs.extend(import_collector::collect_imports(block));
}
}
Materialized::RawWithImports { types, .. } => {
for ty in types {
ty.collect_imports(&mut import_refs);
}
}
Materialized::Raw(_) => {}
}
}
let explicit_entries: Vec<_> = self
.explicit_imports
.iter()
.cloned()
.map(|spec| spec.into_entry())
.collect();
let imports = ImportGroup::resolve_with_explicit(&import_refs, explicit_entries);
let mut output = String::new();
if let Some(header) = &self.header {
let mut renderer = CodeRenderer::new(lang, &imports, width);
let header_output = renderer.render(header)?;
if !header_output.is_empty() {
output.push_str(&header_output);
if !header_output.ends_with('\n') {
output.push('\n');
}
output.push('\n');
}
}
let import_header = lang.render_imports(&imports);
if !import_header.is_empty() {
output.push_str(&import_header);
output.push_str("\n\n");
}
for (i, mat) in materialized.iter().enumerate() {
if i > 0 {
output.push('\n');
}
match mat {
Materialized::Blocks(blocks) => {
for (j, block) in blocks.iter().enumerate() {
if j > 0 {
output.push('\n');
}
let mut renderer = CodeRenderer::new(lang, &imports, width);
let member_output = renderer.render(block)?;
output.push_str(&member_output);
if !member_output.ends_with('\n') {
output.push('\n');
}
}
}
Materialized::Raw(content) => {
output.push_str(content);
if !content.ends_with('\n') {
output.push('\n');
}
}
Materialized::RawWithImports { content, .. } => {
output.push_str(content);
if !content.ends_with('\n') {
output.push('\n');
}
}
}
}
Ok(output)
}
}
#[derive(Debug)]
pub struct FileSpecBuilder {
filename: String,
header: Option<CodeBlock>,
members: Vec<FileMember>,
explicit_imports: Vec<ImportSpec>,
lang: Option<Box<dyn CodeLang>>,
}
impl FileSpecBuilder {
pub fn header(mut self, block: CodeBlock) -> Self {
self.header = Some(block);
self
}
pub fn add_code(mut self, block: CodeBlock) -> Self {
self.members.push(FileMember::Code(block));
self
}
pub fn add_raw(mut self, content: &str) -> Self {
self.members
.push(FileMember::RawContent(content.to_string()));
self
}
pub fn add_raw_with_imports(mut self, content: &str, types: Vec<TypeName>) -> Self {
self.members.push(FileMember::RawContentWithImports {
content: content.to_string(),
types,
});
self
}
pub fn add_member(mut self, member: FileMember) -> Self {
self.members.push(member);
self
}
pub fn add_type(mut self, spec: TypeSpec) -> Self {
self.members.push(FileMember::Type(spec));
self
}
pub fn add_function(mut self, spec: FunSpec) -> Self {
self.members.push(FileMember::Fun(spec));
self
}
pub fn add_spec(mut self, spec: impl Emittable + 'static) -> Self {
self.members.push(FileMember::Spec(Box::new(spec)));
self
}
pub fn lang(mut self, lang: impl CodeLang) -> Self {
self.lang = Some(Box::new(lang));
self
}
pub fn add_import(mut self, spec: ImportSpec) -> Self {
self.explicit_imports.push(spec);
self
}
pub fn build(self) -> Result<FileSpec, SigilStitchError> {
snafu::ensure!(
!self.filename.is_empty(),
crate::error::EmptyNameSnafu {
builder: "FileSpecBuilder",
}
);
let lang = self.lang.ok_or_else(|| {
let ext = self.filename.rsplit('.').next().unwrap_or("");
SigilStitchError::Render {
context: "FileSpecBuilder::build()".to_string(),
message: format!(
"unrecognized file extension '.{ext}' in filename '{}'; \
use FileSpec::builder_with() to specify the language explicitly",
self.filename
),
}
})?;
Ok(FileSpec {
filename: self.filename,
header: self.header,
members: self.members,
explicit_imports: self.explicit_imports,
lang: Some(lang),
})
}
}