use crate::import::ImportRef;
use crate::lang::CodeLang;
use crate::type_name::TypeName;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub enum FormatPart {
Literal(String),
Type,
Name,
StringLit,
Literal_,
Wrap,
Indent,
Dedent,
StatementBegin,
StatementEnd,
Newline,
BlockOpen,
BlockClose,
BlockCloseTransition,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(bound = "")]
pub enum Arg<L: CodeLang> {
TypeName(TypeName<L>),
Name(String),
StringLit(String),
Literal(String),
Code(CodeBlock<L>),
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(bound = "")]
pub struct CodeBlock<L: CodeLang> {
pub(crate) parts: Vec<FormatPart>,
pub(crate) args: Vec<Arg<L>>,
}
impl<L: CodeLang> CodeBlock<L> {
pub fn builder() -> CodeBlockBuilder<L> {
CodeBlockBuilder::new()
}
pub fn of(
format: &str,
args: impl IntoArgs<L>,
) -> Result<Self, crate::error::SigilStitchError> {
let mut builder = CodeBlockBuilder::new();
builder.add(format, args);
builder.build()
}
pub fn is_empty(&self) -> bool {
self.parts.is_empty()
}
pub fn collect_imports(&self, out: &mut Vec<ImportRef>) {
for arg in &self.args {
match arg {
Arg::TypeName(tn) => tn.collect_imports(out),
Arg::Code(cb) => cb.collect_imports(out),
_ => {}
}
}
}
}
#[derive(Debug)]
pub struct CodeBlockBuilder<L: CodeLang> {
parts: Vec<FormatPart>,
args: Vec<Arg<L>>,
indent_depth: i32,
errors: Vec<crate::error::SigilStitchError>,
}
impl<L: CodeLang> CodeBlockBuilder<L> {
pub fn new() -> Self {
Self {
parts: Vec::new(),
args: Vec::new(),
indent_depth: 0,
errors: Vec::new(),
}
}
pub fn add(&mut self, format: &str, args: impl IntoArgs<L>) -> &mut Self {
let arg_vec = args.into_args();
let parsed = match parse_format(format) {
Ok(parts) => parts,
Err(err) => {
self.errors.push(err);
return self;
}
};
let consuming_specifiers: Vec<String> = parsed
.iter()
.filter_map(|p| match p {
FormatPart::Type => Some("%T".to_string()),
FormatPart::Name => Some("%N".to_string()),
FormatPart::StringLit => Some("%S".to_string()),
FormatPart::Literal_ => Some("%L".to_string()),
_ => None,
})
.collect();
let expected_args = consuming_specifiers.len();
if expected_args != arg_vec.len() {
let actual_arg_kinds: Vec<String> = arg_vec
.iter()
.map(|a| match a {
Arg::TypeName(_) => "TypeName".to_string(),
Arg::Name(_) => "Name".to_string(),
Arg::StringLit(_) => "StringLit".to_string(),
Arg::Literal(_) => "Literal".to_string(),
Arg::Code(_) => "Code".to_string(),
})
.collect();
self.errors
.push(crate::error::SigilStitchError::FormatArgCount {
format: format.to_string(),
expected: expected_args,
actual: arg_vec.len(),
expected_specifiers: consuming_specifiers,
actual_arg_kinds,
});
return self;
}
self.parts.extend(parsed);
self.args.extend(arg_vec);
self
}
pub fn add_statement(&mut self, format: &str, args: impl IntoArgs<L>) -> &mut Self {
self.parts.push(FormatPart::StatementBegin);
self.add(format, args);
self.parts.push(FormatPart::StatementEnd);
self.parts.push(FormatPart::Newline);
self
}
pub fn begin_control_flow(&mut self, format: &str, args: impl IntoArgs<L>) -> &mut Self {
self.add(format, args);
self.parts.push(FormatPart::BlockOpen);
self.parts.push(FormatPart::Newline);
self.parts.push(FormatPart::Indent);
self.indent_depth += 1;
self
}
pub fn next_control_flow(&mut self, format: &str, args: impl IntoArgs<L>) -> &mut Self {
self.parts.push(FormatPart::Dedent);
self.indent_depth -= 1;
self.parts.push(FormatPart::BlockCloseTransition);
self.add(format, args);
self.parts.push(FormatPart::BlockOpen);
self.parts.push(FormatPart::Newline);
self.parts.push(FormatPart::Indent);
self.indent_depth += 1;
self
}
pub fn end_control_flow(&mut self) -> &mut Self {
self.parts.push(FormatPart::Dedent);
self.indent_depth -= 1;
self.parts.push(FormatPart::BlockClose);
self.parts.push(FormatPart::Newline);
self
}
pub fn add_line(&mut self) -> &mut Self {
self.parts.push(FormatPart::Newline);
self
}
pub fn add_comment(&mut self, text: &str) -> &mut Self {
self.parts
.push(FormatPart::Literal(format!("__COMMENT__{text}")));
self.parts.push(FormatPart::Newline);
self
}
pub fn add_code(&mut self, block: CodeBlock<L>) -> &mut Self {
self.parts.push(FormatPart::Literal_);
self.args.push(Arg::Code(block));
self
}
pub fn build(self) -> Result<CodeBlock<L>, crate::error::SigilStitchError> {
if let Some(err) = self.errors.into_iter().next() {
return Err(err);
}
if self.indent_depth != 0 {
return Err(crate::error::SigilStitchError::UnbalancedIndent {
depth: self.indent_depth,
});
}
Ok(CodeBlock {
parts: self.parts,
args: self.args,
})
}
pub fn build_unwrap(self) -> CodeBlock<L> {
self.build().unwrap()
}
}
impl<L: CodeLang> Default for CodeBlockBuilder<L> {
fn default() -> Self {
Self::new()
}
}
fn parse_format(format: &str) -> Result<Vec<FormatPart>, crate::error::SigilStitchError> {
let mut parts = Vec::new();
let mut current_literal = String::new();
let bytes = format.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' && i + 1 < bytes.len() {
let spec = bytes[i + 1];
let part = match spec {
b'T' => Some(FormatPart::Type),
b'N' => Some(FormatPart::Name),
b'S' => Some(FormatPart::StringLit),
b'L' => Some(FormatPart::Literal_),
b'W' => Some(FormatPart::Wrap),
b'>' => Some(FormatPart::Indent),
b'<' => Some(FormatPart::Dedent),
b'[' => Some(FormatPart::StatementBegin),
b']' => Some(FormatPart::StatementEnd),
b'%' => {
current_literal.push('%');
i += 2;
continue;
}
_ => {
return Err(crate::error::SigilStitchError::InvalidFormatSpecifier {
format: format.to_string(),
specifier: spec as char,
});
}
};
if let Some(part) = part {
if !current_literal.is_empty() {
parts.push(FormatPart::Literal(std::mem::take(&mut current_literal)));
}
parts.push(part);
}
i += 2;
} else if bytes[i] == b'\n' {
if !current_literal.is_empty() {
parts.push(FormatPart::Literal(std::mem::take(&mut current_literal)));
}
parts.push(FormatPart::Newline);
i += 1;
} else {
current_literal.push(bytes[i] as char);
i += 1;
}
}
if !current_literal.is_empty() {
parts.push(FormatPart::Literal(current_literal));
}
Ok(parts)
}
pub trait IntoArgs<L: CodeLang> {
fn into_args(self) -> Vec<Arg<L>>;
}
impl<L: CodeLang> IntoArgs<L> for () {
fn into_args(self) -> Vec<Arg<L>> {
Vec::new()
}
}
impl<L: CodeLang> IntoArgs<L> for TypeName<L> {
fn into_args(self) -> Vec<Arg<L>> {
vec![Arg::TypeName(self)]
}
}
impl<L: CodeLang> IntoArgs<L> for &str {
fn into_args(self) -> Vec<Arg<L>> {
vec![Arg::Literal(self.to_string())]
}
}
impl<L: CodeLang> IntoArgs<L> for String {
fn into_args(self) -> Vec<Arg<L>> {
vec![Arg::Literal(self)]
}
}
impl<L: CodeLang> IntoArgs<L> for CodeBlock<L> {
fn into_args(self) -> Vec<Arg<L>> {
vec![Arg::Code(self)]
}
}
impl<L: CodeLang> IntoArgs<L> for Vec<Arg<L>> {
fn into_args(self) -> Vec<Arg<L>> {
self
}
}
pub struct NameArg(pub String);
impl<L: CodeLang> IntoArgs<L> for NameArg {
fn into_args(self) -> Vec<Arg<L>> {
vec![Arg::Name(self.0)]
}
}
pub struct StringLitArg(pub String);
impl<L: CodeLang> IntoArgs<L> for StringLitArg {
fn into_args(self) -> Vec<Arg<L>> {
vec![Arg::StringLit(self.0)]
}
}
impl<L: CodeLang> From<TypeName<L>> for Arg<L> {
fn from(tn: TypeName<L>) -> Self {
Arg::TypeName(tn)
}
}
impl<L: CodeLang> From<&str> for Arg<L> {
fn from(s: &str) -> Self {
Arg::Literal(s.to_string())
}
}
impl<L: CodeLang> From<String> for Arg<L> {
fn from(s: String) -> Self {
Arg::Literal(s)
}
}
impl<L: CodeLang> From<CodeBlock<L>> for Arg<L> {
fn from(cb: CodeBlock<L>) -> Self {
Arg::Code(cb)
}
}
impl<L: CodeLang> From<NameArg> for Arg<L> {
fn from(n: NameArg) -> Self {
Arg::Name(n.0)
}
}
impl<L: CodeLang> From<StringLitArg> for Arg<L> {
fn from(s: StringLitArg) -> Self {
Arg::StringLit(s.0)
}
}
macro_rules! impl_into_args_tuple {
($($idx:tt $T:ident),+) => {
impl<L: CodeLang, $($T: Into<Arg<L>>),+> IntoArgs<L> for ($($T,)+) {
fn into_args(self) -> Vec<Arg<L>> {
vec![$(self.$idx.into()),+]
}
}
};
}
impl_into_args_tuple!(0 A);
impl_into_args_tuple!(0 A, 1 B);
impl_into_args_tuple!(0 A, 1 B, 2 C);
impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D);
impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E);
impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F);
impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G);
impl_into_args_tuple!(0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G, 7 H);
#[cfg(test)]
mod tests {
use super::*;
use crate::lang::typescript::TypeScript;
#[test]
fn test_parse_all_specifiers() {
let parts = parse_format("hello %T world %N %S %L %W %> %< %[ %]").unwrap();
assert!(parts.contains(&FormatPart::Type));
assert!(parts.contains(&FormatPart::Name));
assert!(parts.contains(&FormatPart::StringLit));
assert!(parts.contains(&FormatPart::Literal_));
assert!(parts.contains(&FormatPart::Wrap));
assert!(parts.contains(&FormatPart::Indent));
assert!(parts.contains(&FormatPart::Dedent));
assert!(parts.contains(&FormatPart::StatementBegin));
assert!(parts.contains(&FormatPart::StatementEnd));
}
#[test]
fn test_parse_literal_percent() {
let parts = parse_format("100%%").unwrap();
assert_eq!(parts, vec![FormatPart::Literal("100%".to_string())]);
}
#[test]
fn test_parse_empty() {
let parts = parse_format("").unwrap();
assert!(parts.is_empty());
}
#[test]
fn test_parse_newlines() {
let parts = parse_format("line1\nline2").unwrap();
assert_eq!(
parts,
vec![
FormatPart::Literal("line1".to_string()),
FormatPart::Newline,
FormatPart::Literal("line2".to_string()),
]
);
}
#[test]
fn test_builder_add_statement() {
let mut b = CodeBlock::<TypeScript>::builder();
b.add_statement("const x = %L", "42");
let block = b.build().unwrap();
assert!(!block.is_empty());
assert!(block.parts.contains(&FormatPart::StatementBegin));
assert!(block.parts.contains(&FormatPart::StatementEnd));
}
#[test]
fn test_builder_control_flow() {
let mut b = CodeBlock::<TypeScript>::builder();
b.begin_control_flow("if (x > 0)", ());
b.add_statement("return x", ());
b.end_control_flow();
let block = b.build().unwrap();
assert!(!block.is_empty());
}
#[test]
fn test_builder_unbalanced_control_flow() {
let mut b = CodeBlock::<TypeScript>::builder();
b.begin_control_flow("if (x)", ());
b.add_statement("y()", ());
let result = b.build();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("unbalanced"));
}
#[test]
fn test_mismatched_arg_count() {
let mut b = CodeBlock::<TypeScript>::builder();
b.add("%T", ());
let result = b.build();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("expects 1 args but got 0")
);
}
#[test]
fn test_into_args_tuple() {
let user = TypeName::<TypeScript>::importable("./models", "User");
let args: Vec<Arg<TypeScript>> = (user, "hello").into_args();
assert_eq!(args.len(), 2);
assert!(matches!(&args[0], Arg::TypeName(_)));
assert!(matches!(&args[1], Arg::Literal(s) if s == "hello"));
}
#[test]
fn test_into_args_single_typename() {
let user = TypeName::<TypeScript>::importable("./models", "User");
let args: Vec<Arg<TypeScript>> = user.into_args();
assert_eq!(args.len(), 1);
}
#[test]
fn test_into_args_single_str() {
let args: Vec<Arg<TypeScript>> = "hello".into_args();
assert_eq!(args.len(), 1);
assert!(matches!(&args[0], Arg::Literal(s) if s == "hello"));
}
#[test]
fn test_collect_imports_from_codeblock() {
let user = TypeName::<TypeScript>::importable("./models", "User");
let tag = TypeName::<TypeScript>::importable("./models", "Tag");
let mut b = CodeBlock::<TypeScript>::builder();
b.add_statement("const u: %T = getUser()", (user,));
b.add_statement("const t: %T = getTag()", (tag,));
let block = b.build().unwrap();
let mut imports = Vec::new();
block.collect_imports(&mut imports);
assert_eq!(imports.len(), 2);
assert_eq!(imports[0].name, "User");
assert_eq!(imports[1].name, "Tag");
}
#[test]
fn test_nested_codeblock_imports() {
let user = TypeName::<TypeScript>::importable("./models", "User");
let mut ib = CodeBlock::<TypeScript>::builder();
ib.add_statement("return new %T()", (user,));
let inner = ib.build().unwrap();
let mut ob = CodeBlock::<TypeScript>::builder();
ob.add_code(inner);
let outer = ob.build().unwrap();
let mut imports = Vec::new();
outer.collect_imports(&mut imports);
assert_eq!(imports.len(), 1);
assert_eq!(imports[0].name, "User");
}
#[test]
fn test_name_arg() {
let mut b = CodeBlock::<TypeScript>::builder();
b.add("this.%N()", (NameArg("getUser".to_string()),));
let block = b.build().unwrap();
assert_eq!(block.args.len(), 1);
assert!(matches!(&block.args[0], Arg::Name(s) if s == "getUser"));
}
#[test]
fn test_string_lit_arg() {
let mut b = CodeBlock::<TypeScript>::builder();
b.add("const x = %S", (StringLitArg("hello".to_string()),));
let block = b.build().unwrap();
assert_eq!(block.args.len(), 1);
assert!(matches!(&block.args[0], Arg::StringLit(s) if s == "hello"));
}
#[test]
fn test_invalid_format_specifier() {
let mut b = CodeBlock::<TypeScript>::builder();
b.add("hello %X world", ());
let result = b.build();
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("invalid format specifier"));
assert!(err_msg.contains("%X"));
}
#[test]
fn test_parse_format_invalid_specifier_returns_error() {
let result = parse_format("foo %Z bar");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("invalid format specifier"));
assert!(err_msg.contains("%Z"));
}
#[test]
fn test_mismatched_arg_count_includes_specifiers_and_kinds() {
let user = TypeName::<TypeScript>::importable("./models", "User");
let mut b = CodeBlock::<TypeScript>::builder();
b.add("%T %S %L", (user,));
let result = b.build();
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("expects 3 args but got 1"));
assert!(err_msg.contains("%T"));
assert!(err_msg.contains("%S"));
assert!(err_msg.contains("%L"));
assert!(err_msg.contains("TypeName"));
}
}