use core::fmt::Write;
use alloc::format;
use indoc::writedoc;
use crate::{
stream::{FmtWriter, StreamError, Writes},
types::{Coda, Text, Type, Unspecified},
};
pub fn generate_types(coda: &Coda, stream: &mut impl Writes) -> Result<(), StreamError> {
let coda_type_name = format!("{}Data", coda.local_name.trim());
let coda_type_docs = match &coda.docs {
Some(docs) => docs.trim(),
None => "Undocumented Coda. How could you? ;~;",
};
let mut writer = FmtWriter::from(stream);
write_typescript_doc(&mut writer, 0, coda_type_docs)?;
let _ = writedoc!(
writer,
r#"
const {coda_type_name} = {{
"#
);
for data_type in [Unspecified::DATA_TYPE].iter().chain(coda.iter()) {
let data_type_name = &data_type.name;
let data_type_docs = match &data_type.docs {
Some(docs) => docs.trim(),
None => "Undocumented Data. How could you? ;~;",
};
write_typescript_doc(&mut writer, 4, data_type_docs)?;
let _ = writeln!(writer, " {data_type_name}(): {data_type_name} {{");
let _ = writeln!(writer, " return new {data_type_name}();");
let _ = write!(writer, " }},\n\n");
}
let _ = writeln!(writer, " /**");
let _ = writeln!(
writer,
" * Executes the matching handler in `matcher` for `data`."
);
let _ = writeln!(writer, " *");
let _ = writeln!(writer, " * @remarks");
let _ = writeln!(writer, " *");
let _ = writeln!(
writer,
" * If `data` does not match any of the handlers in `matcher`,"
);
let _ = writeln!(
writer,
" * the {{@link {coda_type_name}Matcher.Unspecified}} handler will be called."
);
let _ = writeln!(writer, " */");
let _ = writeln!(
writer,
" match<R>(data: {coda_type_name}, matcher: {coda_type_name}Matcher<R>): R {{"
);
let _ = writeln!(writer, " switch (data.__ordinal) {{");
for (ordinal, data_type) in coda.iter().enumerate() {
let ordinal = ordinal + 1;
let data_type_name = &data_type.name;
let _ = writeln!(writer, " case {ordinal}: return matcher.{data_type_name} ? matcher.{data_type_name}(data as {data_type_name}) : matcher.Unspecified();");
}
let _ = writeln!(writer, " default: return matcher.Unspecified();");
let _ = writeln!(writer, " }}");
let _ = writeln!(writer, " }},");
let _ = write!(writer, "}};\n\n");
let _ = write!(writer, "type {coda_type_name} = Unspecified");
for data_type in coda.iter() {
let _ = write!(writer, " | {}", data_type.name);
}
let _ = write!(writer, ";\n\n");
let _ = writedoc!(
writer,
r#"
/**
* Parameter for {{@link {coda_type_name}.match}}.
*
* @remarks
*
* Each function on this interface corresponds to one
* of the data types within a {{@link {coda_type_name}}}. When a
* given data type is matched, its corresponding function
* is called.
*
* Every function is optional _except_ `Unspecified`, which
* is called by default when no other function matches a
* given {{@link {coda_type_name}}}.
*
* The functions may return any kind of data, but all
* functions must return the _same_ type of data.
*/
interface {coda_type_name}Matcher<R> {{
/**
* Handler for {{@link Unspecified}} or otherwise unknown data.
*/
Unspecified: () => R;
"#
);
for data_type in coda.iter() {
let data_type_name = &data_type.name;
let _ = writeln!(writer);
write_typescript_doc(
&mut writer,
4,
&format!("Handler for {{@link {data_type_name}}} data."),
)?;
let _ = writeln!(
writer,
" {data_type_name}?: (data: {data_type_name}) => R;"
);
}
let _ = write!(writer, "}}\n\n");
for data_type in [Unspecified::DATA_TYPE].iter().chain(coda.iter()) {
let ordinal = data_type.format().as_data_format().ordinal;
let data_type_name = &data_type.name;
let _ = writedoc!(
writer,
r#"
/**
* Class corresponding to {{@link {coda_type_name}.{data_type_name}}}.
*/
class {data_type_name} {{
readonly __ordinal: {ordinal} = {ordinal};
"#
);
for field in data_type.iter() {
let field_name = &field.name;
let field_docs = match &field.docs {
Some(docs) => docs.trim(),
None => "Undocumented Field. How could you? ;~;",
};
let field_type = typescript_type(&field.typing);
let _ = writeln!(writer);
write_typescript_doc(&mut writer, 4, field_docs)?;
if field.optional {
let _ = writeln!(writer, " {field_name}?: {field_type} = undefined;");
} else {
let field_default = typescript_default_val(&field.typing);
let _ = writeln!(writer, " {field_name}: {field_type} = {field_default};");
}
}
let _ = write!(writer, "}}\n\n");
}
Ok(())
}
fn write_typescript_doc<W: Writes>(
writer: &mut FmtWriter<'_, W>,
indentation: usize,
docs: &str,
) -> Result<(), StreamError> {
write_indentation(writer, indentation)?;
let _ = writeln!(writer, "/**");
for line in docs.lines() {
write_indentation(writer, indentation)?;
let _ = writeln!(writer, " * {line}");
}
write_indentation(writer, indentation)?;
let _ = writeln!(writer, " */");
Ok(())
}
fn write_indentation<W: Writes>(
writer: &mut FmtWriter<'_, W>,
indentation: usize,
) -> Result<(), StreamError> {
for _ in 0..indentation {
let _ = write!(writer, " ");
}
Ok(())
}
fn typescript_default_val(typing: &Type) -> Text {
match typing {
Type::Unspecified => Text::Static("undefined"),
Type::U8 => Text::Static("0"),
Type::U16 => Text::Static("0"),
Type::U32 => Text::Static("0"),
Type::U64 => Text::Static("0"),
Type::I8 => Text::Static("0"),
Type::I16 => Text::Static("0"),
Type::I32 => Text::Static("0"),
Type::I64 => Text::Static("0"),
Type::F32 => Text::Static("0.0"),
Type::F64 => Text::Static("0.0"),
Type::Bool => Text::Static("false"),
Type::Text => Text::Static("\"\""),
Type::Data(typing) => {
let name = &typing.name;
format!("new {name}()").into()
}
Type::List(_) => Text::Static("[]"),
Type::Map(_) => Text::Static("new Map()"),
}
}
fn typescript_type(typing: &Type) -> Text {
match typing {
Type::Unspecified => Text::Static("unknown"),
Type::U8 => Text::Static("number"),
Type::U16 => Text::Static("number"),
Type::U32 => Text::Static("number"),
Type::U64 => Text::Static("number"),
Type::I8 => Text::Static("number"),
Type::I16 => Text::Static("number"),
Type::I32 => Text::Static("number"),
Type::I64 => Text::Static("number"),
Type::F32 => Text::Static("number"),
Type::F64 => Text::Static("number"),
Type::Bool => Text::Static("boolean"),
Type::Text => Text::Static("string"),
Type::Data(typing) => typing.name.clone(),
Type::List(typing) => {
let typing = typescript_type(typing.as_ref());
format!("Array<{typing}>").into()
}
Type::Map(typing) => {
let key_typing = typescript_type(&typing.as_ref().0);
let value_typing = typescript_type(&typing.as_ref().1);
format!("Map<{key_typing}, {value_typing}>").into()
}
}
}