///|
/// The `@moonorm.ColumnType` for a field type, or `None` when the type has no
/// single-column storage class (`Array`, `Map`, a nested `type`, `Float`, unsigned
/// ints — none map onto one `@moondb.Value`). Only a type in this set gets a
/// declared column, a `from_row` read, and a `to_columns` bind, so a model is only
/// generated when every field is storable.
fn storage_class(type_ : String) -> String? {
  match type_ {
    "Int" => Some("IntType")
    "Int64" => Some("IntType")
    "String" => Some("TextType")
    "Double" => Some("RealType")
    "Bool" => Some("BoolType")
    "Bytes" => Some("BlobType")
    _ => None
  }
}

///|
/// The typed `@moondb.Row` accessor that reads column `idx` as `type_`. The
/// accessors raise `@moondb.DbError` on a type or index mismatch, so a decode bug
/// surfaces at the call site rather than reading a silent zero.
fn row_read(type_ : String, idx : Int) -> String {
  let i = idx.to_string()
  match type_ {
    "Int" => "row.int(" + i + ")"
    "Int64" => "row.int64(" + i + ")"
    "String" => "row.text(" + i + ")"
    "Double" => "row.double(" + i + ")"
    "Bool" => "row.bool(" + i + ")"
    "Bytes" => "row.blob(" + i + ")"
    _ => "row.get(" + i + ")"
  }
}

///|
/// The `@moondb.Value` constructor that boxes `expr` (a `r.field` access) for a
/// column of `type_`, e.g. `Int64` → `@moondb.Int64(r.id)`.
fn value_ctor(type_ : String, expr : String) -> String {
  match type_ {
    "Int" => "@moondb.Int(" + expr + ")"
    "Int64" => "@moondb.Int64(" + expr + ")"
    "String" => "@moondb.Text(" + expr + ")"
    "Double" => "@moondb.Double(" + expr + ")"
    "Bool" => "@moondb.Bool(" + expr + ")"
    "Bytes" => "@moondb.Blob(" + expr + ")"
    _ => "@moondb.Null"
  }
}

///|
/// Whether every field of a `type` block has a storage class, so a full
/// `@moonorm.Model` (with `from_row` / `to_columns`) can be generated for it.
fn all_storable(fields : Array[Field]) -> Bool {
  for f in fields {
    if storage_class(f.type_) is None {
      return false
    }
  }
  true
}

///|
/// Generate a moonorm data layer for every `type` block in `spec`. A block whose
/// fields are all storable yields: the model `struct`; a
/// `_model : @moonorm.Model[T]` built with `@moonorm.Model::new` — declared
/// columns (an `id` column is the primary key), a `from_row` decoder, and the
/// `to_columns` projection an INSERT binds; a `
_table : @moonorm.Table` /// descriptor; and a `
_up` / `
_down` migration pair (create the table /// idempotently / drop it). This is the explicit MoonBit stand-in for a SQLAlchemy /// declarative class plus an Alembic revision, which reflection would otherwise /// synthesise. A block with a non-storable field (a slice, map, or nested message) /// still gets its struct and a plain `@moonorm.Table` descriptor. /// /// The output compiles against `Lfan-ke/moonorm` + `Lfan-ke/moondb`; a consuming /// package imports both. pub fn generate_model(spec : Spec) -> String { let mut out = "// Code generated by moonctl. DO NOT EDIT.\n\n" for t in spec.types { out = out + "///|\n/// `" + t.name + "` model (generated from the `.api` `type` block).\npub(all) struct " + t.name + " {\n" for f in t.fields { out = out + " " + f.name + " : " + f.type_ + "\n" } out = out + "}\n\n" let table = to_snake(t.name) if all_storable(t.fields) { out = out + emit_model(t, table) + emit_migration(table) } else { let cols : Array[String] = [] for f in t.fields { cols.push(quote(to_snake(f.name))) } out = out + "///|\n/// Table metadata for `" + t.name + "` (a non-storable field — slice, map, or nested message — means no row\n/// mapper, so only the descriptor is generated).\npub let " + table + "_table : @moonorm.Table = {\n name: " + quote(table) + ",\n columns: [" + commas(cols) + "],\n}\n\n" } } out } ///| /// Emit the `@moonorm.Model[T]` value and the `@moonorm.Table` descriptor for a /// fully storable `type` block. fn emit_model(t : TypeDef, table : String) -> String { let col_lines : Array[String] = [] for f in t.fields { let cname = to_snake(f.name) let cls = match storage_class(f.type_) { Some(c) => c None => "TextType" } let pk = if cname == "id" { ", primary_key=true" } else { "" } col_lines.push( " @moonorm.column(" + quote(cname) + ", @moonorm." + cls + pk + "),", ) } let from_fields : Array[String] = [] for i = 0; i < t.fields.length(); i = i + 1 { let f = t.fields[i] from_fields.push(" " + f.name + ": " + row_read(f.type_, i) + ",") } let to_pairs : Array[String] = [] for f in t.fields { to_pairs.push( " (" + quote(to_snake(f.name)) + ", " + value_ctor(f.type_, "r." + f.name) + "),", ) } "///|\n/// Declarative model for `" + t.name + "`: declared columns, a `@moondb.Row` decoder, and the column projection an\n/// INSERT binds. The explicit stand-in for a SQLAlchemy declarative class.\npub let " + table + "_model : @moonorm.Model[" + t.name + "] = @moonorm.Model::new(\n " + quote(table) + ",\n [\n" + join_lines(col_lines) + "\n ],\n row => {\n" + join_lines(from_fields) + "\n },\n r => [\n" + join_lines(to_pairs) + "\n ],\n)\n\n" + "///|\n/// Table metadata for `" + t.name + "` (name + column list), for the plain builder API.\npub let " + table + "_table : @moonorm.Table = " + table + "_model.table_descriptor()\n\n" } ///| /// Emit the `
_up` / `
_down` migration pair for a storable model: /// create the table idempotently, drop it. This is the explicit equivalent of an /// Alembic `upgrade()` / `downgrade()` revision. fn emit_migration(table : String) -> String { "///|\n/// Migration up: create the `" + table + "` table (idempotent).\npub fn " + table + "_up(sess : @moonorm.Session) -> @moondb.ExecResult raise @moondb.DbError {\n sess.create_table(" + table + "_model, if_not_exists=true)\n}\n\n" + "///|\n/// Migration down: drop the `" + table + "` table.\npub fn " + table + "_down(sess : @moonorm.Session) -> @moondb.ExecResult raise @moondb.DbError {\n sess.execute(" + quote("DROP TABLE IF EXISTS " + table) + ", [])\n}\n\n" } ///| /// Join lines with `\n` (no trailing newline) — the codegen counterpart of /// `commas` for multi-line blocks. fn join_lines(parts : Array[String]) -> String { let mut out = "" for i = 0; i < parts.length(); i = i + 1 { if i > 0 { out = out + "\n" } out = out + parts[i] } out }