use std::fmt::{self, Write};
use convert_case::{Case, Casing};
use spacetimedb_lib::type_def::{PrimitiveType, ReducerDef, TableDef};
use spacetimedb_lib::{ElementDef, TupleDef, TypeDef};
use super::code_indenter::CodeIndenter;
use super::INDENT;
const NAMESPACE: &str = "SpacetimeDB";
fn primitive_to_csharp(prim: PrimitiveType) -> &'static str {
match prim {
PrimitiveType::Bool => "bool",
PrimitiveType::I8 => "sbyte",
PrimitiveType::U8 => "byte",
PrimitiveType::I16 => "short",
PrimitiveType::U16 => "ushort",
PrimitiveType::I32 => "int",
PrimitiveType::U32 => "uint",
PrimitiveType::I64 => "long",
PrimitiveType::U64 => "ulong",
PrimitiveType::I128 => panic!("i128 not supported for csharp"),
PrimitiveType::U128 => panic!("i128 not supported for csharp"),
PrimitiveType::String => "string",
PrimitiveType::F32 => "float",
PrimitiveType::F64 => "double",
PrimitiveType::Bytes => "byte[]",
PrimitiveType::Hash => "SpacetimeDB.Hash",
PrimitiveType::Unit => todo!(), }
}
fn ty_fmt(ty: &TypeDef) -> impl fmt::Display + '_ {
fmt_fn(move |f| match ty {
TypeDef::Tuple(tup) => f.write_str(csharp_tuplename(tup)),
TypeDef::Enum(_) => unimplemented!(),
TypeDef::Vec { element_type } => write!(f, "System.Collections.Generic.List<{}>", ty_fmt(element_type)),
TypeDef::Primitive(prim) => f.write_str(primitive_to_csharp(*prim)),
})
}
fn csharp_tuplename(tup: &TupleDef) -> &str {
tup.name.as_deref().expect("tuples should have names")
}
fn fmt_fn(f: impl Fn(&mut fmt::Formatter) -> fmt::Result) -> impl fmt::Display {
struct FDisplay<F>(F);
impl<F: Fn(&mut fmt::Formatter) -> fmt::Result> fmt::Display for FDisplay<F> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
(self.0)(f)
}
}
FDisplay(f)
}
macro_rules! indent_scope {
($x:ident) => {
let mut $x = $x.indented(1);
};
}
fn convert_typedef(ty: &TypeDef) -> impl fmt::Display + '_ {
fmt_fn(move |f| match ty {
TypeDef::Tuple(tup) => {
write!(f, "{}.GetTypeDef()", csharp_tuplename(tup))
}
TypeDef::Enum(_) => unimplemented!(),
TypeDef::Vec { element_type } => {
write!(f, "SpacetimeDB.TypeDef.GetVec({})", convert_typedef(element_type))
}
TypeDef::Primitive(prim) => write!(f, "SpacetimeDB.TypeDef.BuiltInType(SpacetimeDB.TypeDef.Def.{:?})", prim),
})
}
fn convert_elementdef(elem: &ElementDef) -> impl fmt::Display + '_ {
fmt_fn(move |f| {
write!(
f,
"new SpacetimeDB.ElementDef({}, {})",
elem.tag,
convert_typedef(&elem.element_type)
)
})
}
fn convert_tupledef(tuple: &TupleDef) -> impl fmt::Display + '_ {
fmt_fn(move |f| {
writeln!(f, "TypeDef.Tuple(new ElementDef[]")?;
writeln!(f, "{{")?;
for elem in &tuple.elements {
writeln!(f, "{INDENT}{},", convert_elementdef(elem))?;
}
write!(f, "}})")
})
}
pub fn autogen_csharp_tuple(name: &str, tuple: &TupleDef) -> String {
autogen_csharp_tuple_table_common(name, tuple, None)
}
pub fn autogen_csharp_table(name: &str, table: &TableDef) -> String {
autogen_csharp_tuple_table_common(name, &table.tuple, Some(&table.unique_columns))
}
fn autogen_csharp_tuple_table_common(name: &str, tuple: &TupleDef, unique_columns: Option<&[u8]>) -> String {
let mut output = CodeIndenter::new(String::new());
let struct_name_pascal_case = name.to_case(Case::Pascal);
writeln!(
output,
"// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE",
)
.unwrap();
writeln!(output, "// WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD.").unwrap();
writeln!(output).unwrap();
writeln!(output, "namespace {NAMESPACE}").unwrap();
writeln!(output, "{{").unwrap();
{
indent_scope!(output);
writeln!(
output,
"public partial class {struct_name_pascal_case} : IDatabaseTable"
)
.unwrap();
writeln!(output, "{{").unwrap();
{
indent_scope!(output);
for field in &tuple.elements {
let field_name = field.name.as_ref().expect("autogen'd tuples should have field names");
writeln!(output, "[Newtonsoft.Json.JsonProperty(\"{field_name}\")]").unwrap();
writeln!(
output,
"public {} {};",
ty_fmt(&field.element_type),
field_name.to_case(Case::Camel)
)
.unwrap();
}
writeln!(output, "public static TypeDef GetTypeDef()").unwrap();
writeln!(output, "{{").unwrap();
{
indent_scope!(output);
writeln!(output, "return {};", convert_tupledef(&tuple)).unwrap();
}
writeln!(output, "}}").unwrap();
writeln!(output).unwrap();
write!(
output,
"{}",
autogen_csharp_tuple_to_struct(&struct_name_pascal_case, tuple)
)
.unwrap();
if let Some(unique_columns) = unique_columns {
autogen_csharp_access_funcs_for_struct(
&mut output,
&struct_name_pascal_case,
tuple,
name,
unique_columns,
);
}
}
writeln!(output, "}}").unwrap();
}
writeln!(output, "}}").unwrap();
output.into_inner()
}
fn autogen_csharp_tuple_to_struct(struct_name_pascal_case: &str, tuple: &TupleDef) -> String {
let mut output_contents_header: String = String::new();
let mut vec_conversion: String = String::new();
let mut output_contents_return: String = String::new();
writeln!(
output_contents_header,
"public static explicit operator {struct_name_pascal_case}(TypeValue value)",
)
.unwrap();
writeln!(output_contents_header, "{{").unwrap();
writeln!(
output_contents_header,
"\tvar tupleValue = value.GetValue(TypeDef.Def.Tuple) as TypeValue[];"
)
.unwrap();
writeln!(output_contents_header, "\tif (tupleValue == null)").unwrap();
writeln!(output_contents_header, "\t{{").unwrap();
writeln!(
output_contents_header,
"\t\tthrow new System.InvalidOperationException($\"Invalid value (must be Tuple): {{value.TypeDef.Type}}\");"
)
.unwrap();
writeln!(output_contents_header, "\t}}").unwrap();
writeln!(output_contents_header).unwrap();
writeln!(output_contents_return, "\treturn new {}", struct_name_pascal_case).unwrap();
writeln!(output_contents_return, "\t{{").unwrap();
for field in &tuple.elements {
let field_name = field.name.as_ref().expect("autogen'd tuples should have field names");
let field_type = &field.element_type;
let csharp_type = ty_fmt(field_type);
let csharp_field_name = field_name.to_string().to_case(Case::Camel);
match field_type {
TypeDef::Tuple(tup) => {
let name = csharp_tuplename(tup);
writeln!(
output_contents_return,
"\t\t{} = ({name})tupleValue[{}],",
csharp_field_name, field.tag,
)
.unwrap();
}
TypeDef::Enum(_) => unimplemented!(),
TypeDef::Primitive(prim) => {
writeln!(
output_contents_return,
"\t\t{} = ({})tupleValue[{}].GetValue(TypeDef.Def.{:?}),",
csharp_field_name, csharp_type, field.tag, prim
)
.unwrap();
}
TypeDef::Vec { element_type } => match &**element_type {
TypeDef::Tuple(tup) => {
let name = csharp_tuplename(tup);
writeln!(
vec_conversion,
"\tvar {}_vec = new System.Collections.Generic.List<{name}>();",
field_name
)
.unwrap();
writeln!(
vec_conversion,
"\tvar {}_vec_source = tupleValue[{}].GetValue(SpacetimeDB.TypeDef.Def.Vec) as System.Collections.Generic.List<SpacetimeDB.TypeValue>;",
field_name, field.tag
).unwrap();
writeln!(vec_conversion, "\tforeach(var entry in {}_vec_source!)", field_name).unwrap();
writeln!(vec_conversion, "\t{{").unwrap();
writeln!(vec_conversion, "\t\t{}_vec.Add(({name})entry);", field_name).unwrap();
writeln!(vec_conversion, "\t}}").unwrap();
writeln!(
output_contents_return,
"\t\t{} = {}_vec,",
csharp_field_name, field_name
)
.unwrap();
}
TypeDef::Enum(_) => unimplemented!(),
TypeDef::Primitive(prim) => {
let csharp_type = primitive_to_csharp(*prim);
writeln!(
vec_conversion,
"\tvar {}_vec = new System.Collections.Generic.List<{}>();",
field_name, csharp_type
)
.unwrap();
writeln!(
vec_conversion,
"\tvar {}_vec_source = tupleValue[{}].GetValue(TypeDef.Def.Vec) as System.Collections.Generic.List<SpacetimeDB.TypeValue>;",
field_name, field.tag
).unwrap();
writeln!(vec_conversion, "\tforeach(var entry in {}_vec_source!)", field_name).unwrap();
writeln!(vec_conversion, "\t{{").unwrap();
if let PrimitiveType::String = prim {
writeln!(
vec_conversion,
"\t\t{}_vec.Add(entry.GetValue(TypeDef.Def.{:?}) as string);",
field_name, prim,
)
.unwrap();
} else {
writeln!(
vec_conversion,
"\t\t{}_vec.Add(({})entry.GetValue(TypeDef.Def.{:?}));",
field_name, csharp_type, prim,
)
.unwrap();
}
writeln!(vec_conversion, "\t}}").unwrap();
writeln!(
output_contents_return,
"\t\t{} = {}_vec,",
csharp_field_name, field_name
)
.unwrap();
}
TypeDef::Vec { .. } => panic!("nested vecs are disallowed?"),
},
}
}
writeln!(output_contents_return, "\t}};").unwrap();
writeln!(output_contents_return, "}}").unwrap();
output_contents_header + &vec_conversion + &output_contents_return
}
fn autogen_csharp_access_funcs_for_struct(
output: &mut CodeIndenter<String>,
struct_name_pascal_case: &str,
tuple: &TupleDef,
table_name: &str,
unique_columns: &[u8],
) {
let it = Iterator::chain(
unique_columns.iter().copied().zip(std::iter::repeat(true)),
(0..tuple.elements.len())
.map(|i| i as u8)
.filter(|i| unique_columns.binary_search(i).is_err())
.zip(std::iter::repeat(false)),
);
for (col_i, is_unique) in it {
let field = &tuple.elements[col_i as usize];
let field_name = field.name.as_ref().expect("autogen'd tuples should have field names");
let field_type = &field.element_type;
let csharp_field_name_pascal = field_name.to_case(Case::Pascal);
let field_type = match field_type {
TypeDef::Tuple(_) => {
continue;
}
TypeDef::Enum(_) => unimplemented!(),
TypeDef::Primitive(prim) => *prim,
TypeDef::Vec { .. } => {
continue;
}
};
let csharp_field_type = primitive_to_csharp(field_type);
let filter_return_type = fmt_fn(|f| {
if is_unique {
f.write_str(&struct_name_pascal_case)
} else {
write!(f, "System.Collections.Generic.IEnumerable<{}>", struct_name_pascal_case)
}
});
writeln!(
output,
"public static {filter_return_type} FilterBy{}({} value)",
csharp_field_name_pascal, csharp_field_type
)
.unwrap();
writeln!(output, "{{").unwrap();
{
indent_scope!(output);
writeln!(
output,
"foreach(var entry in NetworkManager.clientDB.GetEntries(\"{}\"))",
table_name
)
.unwrap();
writeln!(output, "{{").unwrap();
{
indent_scope!(output);
writeln!(
output,
"var tupleArr = entry.GetValue(TypeDef.Def.Tuple) as TypeValue[];"
)
.unwrap();
writeln!(output, "if (tupleArr == null) continue;").unwrap();
writeln!(
output,
"var compareValue = ({})tupleArr[{}].GetValue(TypeDef.Def.{:?});",
csharp_field_type, field.tag, field_type
)
.unwrap();
writeln!(output, "if (compareValue == value)").unwrap();
{
indent_scope!(output);
if is_unique {
writeln!(output, "return ({struct_name_pascal_case})entry;").unwrap();
} else {
writeln!(output, "yield return ({struct_name_pascal_case})entry;").unwrap();
}
}
}
writeln!(output, "}}").unwrap();
if is_unique {
writeln!(output, "return null;").unwrap();
}
}
writeln!(output, "}}").unwrap();
writeln!(output).unwrap();
}
}
pub fn autogen_csharp_reducer(reducer: &ReducerDef) -> String {
let func_name = reducer.name.as_ref().expect("reducer should have name");
let use_namespace = true;
let func_name_pascal_case = func_name.as_ref().to_case(Case::Pascal);
let mut output = CodeIndenter::new(String::new());
let mut func_arguments: String = String::new();
let mut arg_names: String = String::new();
writeln!(
output,
"// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE"
)
.unwrap();
writeln!(output, "// WILL NOT BE SAVED. MODIFY TABLES IN RUST INSTEAD.").unwrap();
writeln!(output).unwrap();
if use_namespace {
writeln!(output, "namespace {NAMESPACE}").unwrap();
writeln!(output, "{{").unwrap();
output.indent(1);
}
writeln!(output, "public static partial class Reducer").unwrap();
writeln!(output, "{{").unwrap();
{
indent_scope!(output);
for (arg_i, arg) in reducer.args.iter().enumerate() {
let name = arg.name.as_deref().expect("reducer args should have names");
let arg_name = name.to_case(Case::Camel);
if arg_i > 0 {
func_arguments.push_str(", ");
arg_names.push_str(", ");
}
write!(func_arguments, "{} {}", ty_fmt(&arg.element_type), arg_name).unwrap();
arg_names.push_str(&arg_name);
}
writeln!(output, "public static void {func_name_pascal_case}({func_arguments})").unwrap();
writeln!(output, "{{").unwrap();
{
indent_scope!(output);
writeln!(
output,
"NetworkManager.instance.InternalCallReducer(new NetworkManager.Message",
)
.unwrap();
{
writeln!(output, "{{").unwrap();
{
indent_scope!(output);
writeln!(output, "fn = \"{func_name}\",").unwrap();
writeln!(output, "args = new object[] {{ {arg_names} }},").unwrap();
}
writeln!(output, "}});").unwrap();
}
}
writeln!(output, "}}").unwrap();
}
writeln!(output, "}}").unwrap();
if use_namespace {
output.dedent(1);
writeln!(output, "}}").unwrap();
}
output.into_inner()
}