///|
/// Package alias the generated code refers to.
///
/// Generated files are ordinary MoonBit source, so the consuming package must
/// import `Allianaab2m/aya`, whose default alias this is.
const CORE = "@aya"
///|
/// Header written at the top of every generated file.
const HEADER = "// Generated by aya-kit. DO NOT EDIT.\n// Edit the annotated entity struct and re-run the generator instead.\n"
///|
/// Emit a complete generated file for a set of entities.
pub fn emit_file(specs : Array[EntitySpec]) -> String {
let buf = StringBuilder()
buf.write_string(HEADER)
for spec in specs {
buf.write_string("\n")
buf.write_string(emit_entity(spec))
}
buf.to_string()
}
///|
/// Emit the four declarations that make one entity usable as a table.
pub fn emit_entity(spec : EntitySpec) -> String {
let buf = StringBuilder()
buf.write_string(emit_cols_struct(spec))
buf.write_string("\n")
buf.write_string(emit_cols_fn(spec))
buf.write_string("\n")
buf.write_string(emit_selection_fn(spec))
buf.write_string("\n")
buf.write_string(emit_binding_fn(spec))
buf.write_string("\n")
buf.write_string(emit_table_fn(spec))
buf.write_string("\n")
buf.write_string(emit_table_of_fn(spec))
buf.write_string("\n")
buf.write_string(emit_primary_key_fn(spec))
buf.to_string()
}
///|
/// `pub struct UserCols { id : @aya.Column[Int], ... }`
fn emit_cols_struct(spec : EntitySpec) -> String {
let buf = StringBuilder()
buf.write_string("///|\n")
buf.write_string("/// Column handles for `\{spec.type_name}`.\n")
buf.write_string("pub struct \{spec.cols_name} {\n")
for f in spec.fields {
buf.write_string(" \{f.name} : \{CORE}.Column[\{f.ty}]\n")
}
buf.write_string("}\n")
buf.to_string()
}
///|
/// `pub fn User::cols() -> UserCols`
fn emit_cols_fn(spec : EntitySpec) -> String {
let buf = StringBuilder()
buf.write_string("///|\n")
buf.write_string("pub fn \{spec.type_name}::cols() -> \{spec.cols_name} {\n")
buf.write_string(" {\n")
for f in spec.fields {
buf.write_string(
" \{f.name}: \{CORE}.Column::new(tbl=\"\{spec.tbl}\", name=\"\{f.column}\"),\n",
)
}
buf.write_string(" }\n}\n")
buf.to_string()
}
///|
/// `pub fn User::all(cols) -> @aya.Selection[User]`
///
/// Projection and decoder are emitted from one loop over the same field list,
/// so the positional decode cannot drift out of step with the column order.
fn emit_selection_fn(spec : EntitySpec) -> String {
let buf = StringBuilder()
buf.write_string("///|\n")
buf.write_string(
"/// Select every column of `\{spec.type_name}`, in declaration order.\n",
)
buf.write_string(
"pub fn \{spec.type_name}::all(cols : \{spec.cols_name}) -> \{CORE}.Selection[\{spec.type_name}] {\n",
)
buf.write_string(" \{CORE}.Selection::new(\n [\n")
for f in spec.fields {
buf.write_string(" cols.\{f.name}.raw(),\n")
}
buf.write_string(" ],\n row => \{spec.type_name}::{\n")
for i, f in spec.fields {
buf.write_string(
" \{f.name}: \{CORE}.SqlDecode::decode(row[\{i}], \"\{f.column}\"),\n",
)
}
buf.write_string(" },\n )\n}\n")
buf.to_string()
}
///|
/// `pub fn User::binding() -> @aya.Binding[User]`
///
/// The write-side mirror of `all`: same fields, same order, so an INSERT and a
/// SELECT of the same entity agree on what each position means.
fn emit_binding_fn(spec : EntitySpec) -> String {
let buf = StringBuilder()
buf.write_string("///|\n")
buf.write_string(
"/// Write every column of `\{spec.type_name}`, in declaration order.\n",
)
buf.write_string(
"pub fn \{spec.type_name}::binding() -> \{CORE}.Binding[\{spec.type_name}] {\n",
)
buf.write_string(" \{CORE}.Binding::new(\n [")
buf.write_string(spec.fields.map(f => "\"\{f.column}\"").join(", "))
buf.write_string("],\n self => [\n")
for f in spec.fields {
buf.write_string(" \{CORE}.SqlEncode::to_sql_value(self.\{f.name}),\n")
}
buf.write_string(" ],\n )\n}\n")
buf.to_string()
}
///|
/// `pub fn User::table() -> @aya.Table[UserCols, User]`
fn emit_table_fn(spec : EntitySpec) -> String {
let buf = StringBuilder()
buf.write_string("///|\n")
buf.write_string(
"pub fn \{spec.type_name}::table() -> \{CORE}.Table[\{spec.cols_name}, \{spec.type_name}] {\n",
)
buf.write_string(" let cols = \{spec.type_name}::cols()\n")
buf.write_string(
" \{CORE}.Table::new(table_name=\"\{spec.table}\", tbl=\"\{spec.tbl}\", cols~, all=\{spec.type_name}::all(cols), write=\{spec.type_name}::binding())\n",
)
buf.write_string("}\n")
buf.to_string()
}
///|
/// `pub fn[D] UserRow::table_of(to_domain, from_domain) -> Table[UserCols, D]`
///
/// The seam between the row type and a domain type. The table's name, alias
/// and columns come from the annotation; the two mapping functions are the
/// part only the programmer can write, so they are the only arguments.
///
/// `to_domain` may raise: reconstructing a domain value can discover that the
/// stored row does not represent a legal state. `from_domain` cannot.
fn emit_table_of_fn(spec : EntitySpec) -> String {
let buf = StringBuilder()
let row = spec.type_name
buf.write_string("///|\n")
buf.write_string(
"/// Build a table over a domain type mapped to and from `\{row}`.\n",
)
buf.write_string(
"pub fn[D] \{row}::table_of(\n to_domain : (\{row}) -> D raise \{CORE}.DecodeError,\n from_domain : (D) -> \{row},\n) -> \{CORE}.Table[\{spec.cols_name}, D] {\n",
)
buf.write_string(" let cols = \{row}::cols()\n")
buf.write_string(
" \{CORE}.Table::new(\n table_name=\"\{spec.table}\",\n tbl=\"\{spec.tbl}\",\n cols~,\n all=\{row}::all(cols).map(to_domain),\n write=\{row}::binding().contramap(from_domain),\n )\n",
)
buf.write_string("}\n")
buf.to_string()
}
///|
/// `pub fn User::primary_key_name() -> String?`
///
/// Not consumed by the query builder yet; it is what update- and delete-by-id
/// will key on once aya emits DML.
fn emit_primary_key_fn(spec : EntitySpec) -> String {
let pk = spec.fields.filter(f => f.is_primary)
let body = if pk.is_empty() { "None" } else { "Some(\"\{pk[0].column}\")" }
let buf = StringBuilder()
buf.write_string("///|\n")
buf.write_string(
"pub fn \{spec.type_name}::primary_key_name() -> String? {\n \{body}\n}\n",
)
buf.to_string()
}