packages feed

convex-schema-parser-0.1.3.0: src/Backend/Rust.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

module Backend.Rust
  ( generateRustCode,
  )
where

import qualified Convex.Action.Parser as Action
import qualified Convex.Parser as P
import qualified Convex.Schema.Parser as Schema
import Data.Char (isUpper, toLower, toUpper)
import Data.List (intercalate, isInfixOf, isPrefixOf, nub, stripPrefix)
import qualified Data.Map as Map
import PathTree

-- Helper function to prepend a given number of spaces (4 per level).
indent :: Int -> String -> String
indent n s = replicate (n * 4) ' ' ++ s

-- | Top-level function to generate the complete Rust module source.
generateRustCode :: P.ParsedProject -> String
generateRustCode project =
  unlines
    [ "#![allow(dead_code)]",
      "#![allow(non_snake_case)]",
      "// Generated by the Palaba code generator. DO NOT EDIT.",
      "// Save this file as, for example, `src/convex_api.rs`",
      "// and then add `pub mod convex_api;` to your `src/lib.rs` or `src/main.rs`.",
      "//",
      "// Make sure your `Cargo.toml` contains the following dependencies:",
      "// convex = \"0.1.3\"",
      "// serde = { version = \"1.0\", features = [\"derive\"] }",
      "// serde_json = \"1.0\"",
      "// thiserror = \"1.0\"",
      "// anyhow = \"1.0\"",
      "// futures-util = \"0.3\"",
      "",
      generateRustModuleContent project
    ]

-- | Generates the entire content for a single Rust module file.
generateRustModuleContent :: P.ParsedProject -> String
generateRustModuleContent project =
  let (apiClassCode, nestedFromFuncs) = generateApiClass (P.ppFunctions project)
   in unlines
        [ "use convex::{ConvexClient, FunctionResult, Value};",
          "use futures_util::stream::Stream;",
          "use serde::{Deserialize, Deserializer, Serialize, Serializer};",
          "use serde_json;",
          "use std::collections::BTreeMap;",
          "use std::fmt::{self, Display};",
          "use std::marker::PhantomData;",
          "use std::pin::Pin;",
          "use std::task::{Context, Poll};",
          "",
          stripNewlines generateErrorEnum,
          "",
          stripNewlines generateIdStruct,
          "",
          stripNewlines generateFromConvexValueBoilerplate,
          "",
          stripNewlines generateSubscriptionBoilerplate,
          "",
          stripNewlines apiClassCode, -- API class and all submodules
          "",
          stripNewlines $ generateTypesModule project (nub nestedFromFuncs)
        ]

-- | Generates a Rust error enum using `thiserror`.
generateErrorEnum :: String
generateErrorEnum =
  unlines
    [ "/// Represents all possible errors that can occur when interacting with the API.",
      "#[derive(thiserror::Error, Debug)]",
      "pub enum ApiError {",
      indent 1 "#[error(\"Convex client error: {0}\")]",
      indent 1 "ConvexClientError(String),",
      "",
      indent 1 "#[error(\"Convex function error: {0}\")]",
      indent 1 "ConvexFunctionError(String),",
      "",
      indent 1 "#[error(\"Failed to deserialize response: {0}\")]",
      indent 1 "DeserializationError(#[from] serde_json::Error),",
      "",
      indent 1 "#[error(\"Unexpected null value returned from a non-nullable function\")]",
      indent 1 "UnexpectedNullError,",
      "}",
      "",
      "impl From<anyhow::Error> for ApiError {",
      indent 1 "fn from(err: anyhow::Error) -> Self {",
      indent 2 "ApiError::ConvexClientError(err.to_string())",
      indent 1 "}",
      "}"
    ]

-- | Generates the strongly-typed `Id<T>` struct.
generateIdStruct :: String
generateIdStruct =
  unlines
    [ "/// A strongly-typed Convex document ID.",
      "#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]",
      "pub struct Id<T> {",
      indent 1 "id: String,",
      indent 1 "_phantom: PhantomData<T>,",
      "}",
      "",
      "impl<T> Default for Id<T> {",
      indent 1 "fn default() -> Self {",
      indent 2 "Self { id: String::new(), _phantom: PhantomData }",
      indent 1 "}",
      "}",
      "",
      "impl<T> Id<T> {",
      indent 1 "pub fn new(id: String) -> Self {",
      indent 2 "Self { id, _phantom: PhantomData }",
      indent 1 "}",
      "}",
      "",
      "impl<T> Display for Id<T> {",
      indent 1 "fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {",
      indent 2 "write!(f, \"{}\", self.id)",
      indent 1 "}",
      "}",
      "",
      "impl<T> Serialize for Id<T> {",
      indent 1 "fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>",
      indent 1 "where",
      indent 2 "S: Serializer,",
      indent 1 "{",
      indent 2 "serializer.serialize_str(&self.id)",
      indent 1 "}",
      "}",
      "",
      "impl<'de, T> Deserialize<'de> for Id<T> {",
      indent 1 "fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>",
      indent 1 "where",
      indent 2 "D: Deserializer<'de>,",
      indent 1 "{",
      indent 2 "let id = String::deserialize(deserializer)?;",
      indent 2 "Ok(Id::new(id))",
      indent 1 "}",
      "}",
      "",
      "impl<T> From<Id<T>> for Value {",
      indent 1 "fn from(val: Id<T>) -> Self {",
      indent 2 "Value::String(val.id)",
      indent 1 "}",
      "}",
      "impl<T> TryFrom<Value> for Id<T> {",
      indent 1 "type Error = ApiError;",
      "",
      indent 1 "fn try_from(value: Value) -> Result<Self, Self::Error> {",
      indent 2 "if let Value::String(id) = value {",
      indent 3 "Ok(Id::new(id))",
      indent 2 "} else {",
      indent 3 "Err(ApiError::ConvexClientError(",
      indent 4 "\"Expected a string for Id\".to_string(),",
      indent 3 "))",
      indent 2 "}",
      indent 1 "}",
      "}",
      "",
      indent 0 "impl<T> FromConvexValue for Id<T> {",
      indent 1 "fn from_convex(value: Value) -> Result<Self, ApiError> {",
      indent 2 "Id::try_from(value).map_err(ApiError::from)",
      indent 1 "}",
      indent 0 "}"
    ]

-- | Generates the generic TypedSubscription struct and its Stream implementation.
generateSubscriptionBoilerplate :: String
generateSubscriptionBoilerplate =
  unlines
    [ "/// A type-safe, auto-deserializing stream of updates from a Convex query subscription.",
      "#[derive(Debug)]",
      "pub struct TypedSubscription<T> {",
      indent 1 "raw_subscription: convex::QuerySubscription,",
      indent 1 "_phantom: PhantomData<T>,",
      "}",
      "",
      "impl<T> Stream for TypedSubscription<T>",
      "where",
      indent 1 "T: FromConvexValue,",
      "{",
      indent 1 "type Item = Result<T, ApiError>;",
      "",
      indent 1 "fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {",
      indent 2 "let raw_sub_pin: Pin<&mut _> = unsafe { self.map_unchecked_mut(|s| &mut s.raw_subscription) };",
      indent 2 "match raw_sub_pin.poll_next(cx) {",
      indent 3 "Poll::Ready(Some(result)) => {",
      indent 4 "let item = match result {",
      indent 5 "FunctionResult::Value(value) => T::from_convex(value),",
      indent 5 "FunctionResult::ErrorMessage(s) => Err(ApiError::ConvexFunctionError(s)),",
      indent 5 "FunctionResult::ConvexError(err) => Err(ApiError::ConvexClientError(err.to_string())),",
      indent 4 "};",
      indent 4 "Poll::Ready(Some(item))",
      indent 3 "}",
      indent 3 "Poll::Ready(None) => Poll::Ready(None),",
      indent 3 "Poll::Pending => Poll::Pending,",
      indent 2 "}",
      indent 1 "}",
      "}"
    ]

generateFromConvexValueBoilerplate :: String
generateFromConvexValueBoilerplate =
  unlines
    [ "pub trait FromConvexValue: Sized {",
      "    fn from_convex(value: Value) -> Result<Self, ApiError>;",
      "}",
      "",
      "impl FromConvexValue for bool {",
      "    fn from_convex(value: Value) -> Result<Self, ApiError> {",
      "        match value {",
      "            Value::Boolean(b) => Ok(b),",
      "            _ => Err(ApiError::ConvexClientError(\"Expected bool\".into())),",
      "        }",
      "    }",
      "}",
      "",
      "impl FromConvexValue for i64 {",
      "    fn from_convex(value: Value) -> Result<Self, ApiError> {",
      "        match value {",
      "            Value::Int64(i) => Ok(i),",
      "            _ => Err(ApiError::ConvexClientError(\"Expected i64\".into())),",
      "        }",
      "    }",
      "}",
      "",
      "impl FromConvexValue for i32 {",
      "    fn from_convex(value: Value) -> Result<Self, ApiError> {",
      "        match value {",
      "            Value::Int64(i) => i",
      "                .try_into()",
      "                .map_err(|_| ApiError::ConvexClientError(\"i64 out of range for i32\".into())),",
      "            _ => Err(ApiError::ConvexClientError(\"Expected i64\".into())),",
      "        }",
      "    }",
      "}",
      "",
      "impl FromConvexValue for f64 {",
      "    fn from_convex(value: Value) -> Result<Self, ApiError> {",
      "        match value {",
      "            Value::Float64(f) => Ok(f),",
      "            _ => Err(ApiError::ConvexClientError(\"Expected f64\".into())),",
      "        }",
      "    }",
      "}",
      "",
      "impl FromConvexValue for f32 {",
      "    fn from_convex(value: Value) -> Result<Self, ApiError> {",
      "        match value {",
      "            Value::Float64(f) => Ok(f as f32),",
      "            _ => Err(ApiError::ConvexClientError(\"Expected f64\".into())),",
      "        }",
      "    }",
      "}",
      "",
      "impl<T> FromConvexValue for Vec<T>",
      "where",
      "    T: FromConvexValue,",
      "{",
      "    fn from_convex(value: Value) -> Result<Self, ApiError> {",
      "        match value {",
      "            Value::Array(arr) => arr",
      "                .into_iter()",
      "                .map(T::from_convex)",
      "                .collect::<Result<Vec<T>, ApiError>>(),",
      "            _ => Err(ApiError::ConvexClientError(\"Expected array\".into())),",
      "        }",
      "    }",
      "}",
      "",
      "impl<T> FromConvexValue for Option<T>",
      "where",
      "    T: FromConvexValue,",
      "{",
      "    fn from_convex(value: Value) -> Result<Self, ApiError> {",
      "        match value {",
      "            Value::Null => Ok(None),",
      "            other => T::from_convex(other).map(Some),",
      "        }",
      "    }",
      "}",
      "",
      "impl FromConvexValue for Vec<u8> {",
      "    fn from_convex(value: Value) -> Result<Self, ApiError> {",
      "        match value {",
      "            Value::Bytes(bytes) => Ok(bytes),",
      "            _ => Err(ApiError::ConvexClientError(\"Expected bytes\".into())),",
      "        }",
      "    }",
      "}",
      "",
      "impl FromConvexValue for String {",
      "    fn from_convex(value: Value) -> Result<Self, ApiError> {",
      "        match value {",
      "            Value::String(s) => Ok(s),",
      "            _ => Err(ApiError::ConvexClientError(\"Expected string\".into())),",
      "        }",
      "    }",
      "}",
      "",
      "impl FromConvexValue for serde_json::Value {",
      "    fn from_convex(value: Value) -> Result<Self, ApiError> {",
      "        Ok(value.into())",
      "    }",
      "}",
      ""
    ]

generateApiClass :: [Action.ConvexFunction] -> (String, [String])
generateApiClass funcs =
  let tree = buildPathTree funcs
      (structDefs, implDef, nested) = generateApiStructure "Api" tree
   in ( unlines
          [ "pub struct Api {",
            indent 1 "pub client: ConvexClient,",
            "}",
            structDefs,
            implDef
          ],
        nested
      )

generateApiStructure :: String -> PathTree -> (String, String, [String])
generateApiStructure parentName (DirNode dirMap) =
  let (structs, impls, nested) = unzip3 $ map (uncurry processEntry) (Map.toList dirMap)
      (accessors, functions) = partitionEntries (Map.toList dirMap)
   in ( unlines structs,
        unlines
          [ "impl" ++ (if parentName == "Api" then "" else "<'a>") ++ " " ++ parentName ++ (if parentName == "Api" then "" else "<'a>") ++ " {",
            if parentName == "Api" then indent 1 "pub fn new(client: ConvexClient) -> Self {\n        Self { client }\n    }" else "",
            unlines (map generateAccessorMethod accessors),
            unlines (concatMap generateMethodsForEntry functions),
            "}"
          ]
          ++ unlines impls,
        concat nested
      )
  where
    processEntry name (DirNode subDir) =
      let structName = toPascalCase name
          (subStructs, subImpls, nestedFromSub) = generateApiStructure structName (DirNode subDir)
          structDef =
            unlines
              [ "pub struct " ++ structName ++ "<'a> {",
                indent 1 "client: &'a mut ConvexClient,",
                "}"
              ]
       in (unlines [structDef, subStructs], subImpls, nestedFromSub)
    processEntry _ (FuncNode func) =
      let (_, nestedFromFunc) = generateFunction func
          (_, nestedFromSub) = if Action.funcType func == Action.Query then generateSubscriptionFunction func else ("", [])
       in ("", "", nestedFromFunc ++ nestedFromSub)

    partitionEntries =
      foldl
        ( \(ds, fs) (name, node) -> case node of
            DirNode _ -> ((name, node) : ds, fs)
            FuncNode _ -> (ds, (name, node) : fs)
        )
        ([], [])

    generateAccessorMethod (name, _) =
      let structName = toPascalCase name
          methodName = toSnakeCase name
       in unlines
            [ indent 1 ("pub fn " ++ methodName ++ "(&mut self) -> " ++ structName ++ "<'_> {"),
              indent 2 (structName ++ " { client: &mut self.client }"),
              indent 1 "}"
            ]

    generateMethodsForEntry (_, FuncNode func) =
      let (queryDef, _) = generateFunction func
          (subDef, _) =
            if Action.funcType func == Action.Query
              then generateSubscriptionFunction func
              else ("", [])
       in [queryDef, subDef]
    generateMethodsForEntry _ = []
generateApiStructure _ _ = ("", "", [])

generateFunction :: Action.ConvexFunction -> (String, [String])
generateFunction func =
  let funcName = Action.funcName func
      (argSignature, nestedFromArgs) = generateArgSignature funcName (Action.funcArgs func)
      funcNameSnake = toSnakeCase funcName
      (returnHint, isNullable, nestedFromReturn) = getReturnType funcName (Action.funcReturn func)
      handlerCall = case Action.funcType func of
        Action.Query -> "query"
        Action.Mutation -> "mutation"
        Action.Action -> "action"
      fullFuncPath = Action.funcPath func ++ ":" ++ funcName
      btreemapConstruction = generateBTreeMap (Action.funcArgs func)
      returnHandling = generateReturnHandling returnHint isNullable
      funcCode =
        unlines
          [ indent 1 ("/// Wraps the `" ++ fullFuncPath ++ "` " ++ show (Action.funcType func) ++ "."),
            indent 1 ("pub async fn " ++ funcNameSnake ++ "(&mut self, " ++ argSignature ++ ") -> Result<" ++ returnHint ++ ", ApiError> {"),
            btreemapConstruction,
            indent 2 ("let result = self.client." ++ handlerCall ++ "(\"" ++ fullFuncPath ++ "\", btmap).await?;"),
            returnHandling,
            indent 1 "}"
          ]
   in (funcCode, nestedFromArgs ++ nestedFromReturn)

generateSubscriptionFunction :: Action.ConvexFunction -> (String, [String])
generateSubscriptionFunction func =
  let funcName = Action.funcName func
      (argSignature, nestedFromArgs) = generateArgSignature funcName (Action.funcArgs func)
      funcNameSnake = "subscribe_" ++ toSnakeCase funcName
      (returnHint, _, nestedFromReturn) = getReturnType funcName (Action.funcReturn func)
      fullFuncPath = Action.funcPath func ++ ":" ++ funcName
      btreemapConstruction = generateBTreeMap (Action.funcArgs func)
      funcCode =
        unlines
          [ indent 1 ("/// Subscribes to the `" ++ fullFuncPath ++ "` query."),
            indent 1 ("pub async fn " ++ funcNameSnake ++ "(&mut self, " ++ argSignature ++ ") -> Result<TypedSubscription<" ++ returnHint ++ ">, ApiError> {"),
            btreemapConstruction,
            indent 2 ("let raw_subscription = self.client.subscribe(\"" ++ fullFuncPath ++ "\", btmap).await?;"),
            indent 2 "Ok(TypedSubscription {",
            indent 3 "raw_subscription,",
            indent 3 "_phantom: PhantomData,",
            indent 2 "})",
            indent 1 "}"
          ]
   in (funcCode, nestedFromArgs ++ nestedFromReturn)

generateTypesModule :: P.ParsedProject -> [String] -> String
generateTypesModule project nestedFromFuncs =
  let (tableCode, nestedFromTables) = generateAllTables (P.ppSchema project)
      (constantsCode, nestedFromConstants) = generateAllConstants (P.ppConstants project)
      allNestedCode = nub (nestedFromTables ++ nestedFromConstants ++ nestedFromFuncs)
   in unlines
        [ "pub mod types {",
          indent 1 "use super::*;",
          "",
          tableCode,
          constantsCode,
          unlines allNestedCode,
          "}"
        ]

generateAllTables :: Schema.Schema -> (String, [String])
generateAllTables (Schema.Schema tables) =
  let (tableCodes, nested) = unzip $ map generateTableStruct tables
   in (unlines tableCodes, concat nested)

generateTableStruct :: Schema.Table -> (String, [String])
generateTableStruct table =
  let className = toPascalCase (Schema.tableName table) ++ "Doc"
      (fieldLines, nestedFromFields) = unzip $ map (generateField className) (Schema.tableFields table)
   in ( unlines
          [ "#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq)]",
            ("pub struct " ++ className ++ " {"),
            indent 1 "#[serde(default)]",
            indent 1 ("pub _id: Id<" ++ className ++ ">,"),
            indent 1 "#[serde(default)]",
            indent 1 "#[serde(rename = \"_creationTime\")]",
            indent 1 "pub creation_time: f64,",
            unlines fieldLines,
            "}"
          ],
        concat nestedFromFields
      )

generateAllConstants :: Map.Map String Schema.ConvexType -> (String, [String])
generateAllConstants constants =
  let (constCodes, nested) = unzip $ map (uncurry generateConstant) (Map.toList constants)
   in (unlines constCodes, concat nested)

generateConstant :: String -> Schema.ConvexType -> (String, [String])
generateConstant name u@(Schema.VUnion literals)
  | all Schema.isLiteral literals =
      let enumName = toPascalCase name
          enumFromConvexValueImpl = generateFromConvexValueImplEnum ("types::" ++ enumName) literals
          variantNames = map Schema.getLiteralString literals
          buildVariantLines [] = []
          buildVariantLines (first : rest) =
            (indent 2 "#[default]\n" ++ indent 2 ("#[serde(rename = \"" ++ first ++ "\")]\n") ++ indent 2 (toPascalCase first ++ ","))
              : map (\v -> indent 2 ("#[serde(rename = \"" ++ v ++ "\")]\n") ++ indent 2 (toPascalCase v ++ ",")) rest
          code =
            unlines
              [ indent 1 "#[derive(Default, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]",
                indent 1 ("pub enum " ++ enumName ++ " {"),
                unlines $ buildVariantLines variantNames,
                indent 1 "}",
                "",
                enumFromConvexValueImpl,
                ""
              ]
       in (code, [])
  | otherwise =
      let (rustTypeName, nested) = toRustType name u
       in (indent 1 ("pub type " ++ toPascalCase name ++ " = " ++ rustTypeName ++ ";"), nested)
generateConstant name t =
  let (rustTypeName, nested) = toRustType name t
   in (indent 1 ("pub type " ++ toPascalCase name ++ " = " ++ rustTypeName ++ ";"), nested)

generateField :: String -> Schema.Field -> (String, [String])
generateField nameHint field =
  let fieldNameSnake = toSnakeCase (Schema.fieldName field)
      (rustType, nested) = toRustType (nameHint ++ capitalize (Schema.fieldName field)) (Schema.fieldType field)
      serdeRename = if fieldNameSnake /= Schema.fieldName field then indent 2 ("#[serde(rename = \"" ++ Schema.fieldName field ++ "\")]\n") else ""
      serdeAttrs =
        let defaultAttr = "default"
            skipAttr =
              if needsOptionalWrapper (Schema.fieldType field)
                then Just "skip_serializing_if = \"Option::is_none\""
                else Nothing
            allAttrs = case skipAttr of
              Just s -> [defaultAttr, s]
              Nothing -> [defaultAttr]
         in indent 2 ("#[serde(" ++ intercalate ", " allAttrs ++ ")]")
      fieldLine = serdeRename ++ serdeAttrs ++ "\n" ++ indent 2 ("pub " ++ fieldNameSnake ++ ": " ++ rustType ++ ",")
   in (fieldLine, nested)

generateArgSignature :: String -> [(String, Schema.ConvexType)] -> (String, [String])
generateArgSignature funcName args =
  let results = map (\(n, t) -> toRustType (funcName ++ capitalize n) t) args
      sigParts = zipWith (\(n, _) (ty, _) -> toSnakeCase n ++ ": " ++ toRustBorrowType ty) args results
      nestedModels = concatMap snd results
   in (intercalate ", " sigParts, nestedModels)

generateBTreeMap :: [(String, Schema.ConvexType)] -> String
generateBTreeMap [] = indent 2 "let btmap = BTreeMap::new();"
generateBTreeMap btmap =
  let buildStmts (name, convexType) =
        let varName = toSnakeCase name
         in case convexType of
              Schema.VObject _ -> indent 1 ("btmap.insert(\"" ++ name ++ "\".to_string(), " ++ varName ++ ".to_convex_value()?);")
              Schema.VOptional innerConvexType -> indent 1 ("if let Some(v) = " ++ varName ++ " { btmap.insert(\"" ++ name ++ "\".to_string(), " ++ fieldToConvexValue ("v", innerConvexType) ++ "); }")
              _ -> indent 1 ("btmap.insert(\"" ++ name ++ "\".to_string(), " ++ fieldToConvexValue (varName, convexType) ++ ");")
   in unlines
        [ indent 2 "let mut btmap = BTreeMap::new();",
          unlines $ map buildStmts btmap
        ]

fieldToConvexValue :: (String, Schema.ConvexType) -> String
fieldToConvexValue (fieldName, t) =
  let fieldNameSnake = toSnakeCase fieldName
      valueExpr = innerValueToConvexNonOptional fieldNameSnake t
   in valueExpr

toClonedValue :: String -> Schema.ConvexType -> String
toClonedValue varName (Schema.VString) = varName ++ ".to_string()"
toClonedValue varName t
  | isPassedByCopy t = "*" ++ varName
  | otherwise = varName ++ ".clone()"

isPassedByCopy :: Schema.ConvexType -> Bool
isPassedByCopy Schema.VNumber = True
isPassedByCopy Schema.VInt64 = True
isPassedByCopy Schema.VFloat64 = True
isPassedByCopy Schema.VBoolean = True
isPassedByCopy _ = False

getReturnType :: String -> Schema.ConvexType -> (String, Bool, [String])
getReturnType funcName rt =
  let (baseType, nested) = toRustType (funcName ++ "Return") rt
      isNullable = needsOptionalWrapper rt
   in if baseType == "()"
        then ("()", False, nested)
        else (baseType, isNullable, nested)

generateReturnHandling :: String -> Bool -> String
generateReturnHandling "()" _ =
  unlines
    [ indent 2 "match result {",
      indent 3 "FunctionResult::Value(_) => Ok(()),",
      indent 3 "FunctionResult::ErrorMessage(s) => Err(ApiError::ConvexFunctionError(s)),",
      indent 3 "FunctionResult::ConvexError(err) => Err(ApiError::ConvexClientError(err.to_string())),",
      indent 2 "}"
    ]
generateReturnHandling _ isNullable =
  if isNullable
    then
      unlines
        [ indent 2 "match result {",
          indent 3 "FunctionResult::Value(val) => Ok(FromConvexValue::from_convex(val.clone())?),",
          indent 3 "FunctionResult::ErrorMessage(s) => Err(ApiError::ConvexFunctionError(s)),",
          indent 3 "FunctionResult::ConvexError(err) => Err(ApiError::ConvexClientError(err.to_string())),",
          indent 2 "}"
        ]
    else
      unlines
        [ indent 2 "match result {",
          indent 3 "FunctionResult::Value(Value::Null) => Err(ApiError::UnexpectedNullError),",
          indent 3 "FunctionResult::Value(val) => Ok(FromConvexValue::from_convex(val.clone())?),",
          indent 3 "FunctionResult::ErrorMessage(s) => Err(ApiError::ConvexFunctionError(s)),",
          indent 3 "FunctionResult::ConvexError(err) => Err(ApiError::ConvexClientError(err.to_string())),",
          indent 2 "}"
        ]

generateToConvexValueImpl :: String -> [(String, Schema.ConvexType)] -> String
generateToConvexValueImpl structName fields =
  let buildMapInserts (fieldName, fieldType) =
        let conversionBlock = generateFieldToConvexValue (fieldName, fieldType)
         in indent 3 conversionBlock
      mapInserts = unlines $ map buildMapInserts fields
   in unlines
        [ "impl " ++ structName ++ " {",
          indent 1 "pub fn to_convex_value(&self) -> Result<Value, ApiError> {",
          indent 2 "let mut btmap = BTreeMap::new();",
          mapInserts,
          indent 2 "Ok(Value::Object(btmap))",
          indent 1 "}",
          "}"
        ]

generateFromConvexValueImplEnum :: String -> [Schema.ConvexType] -> String
generateFromConvexValueImplEnum structName fields =
  unlines
    [ "impl TryFrom<Value> for " ++ structName ++ " {",
      indent 1 "type Error = ApiError;",
      indent 1 "fn try_from(value: Value) -> Result<Self, Self::Error> {",
      indent 2 "if let Value::String(s) = &value {",
      indent 3 "return match s.as_str() {",
      unlines . map (indent 4) $ generateEnumMatchCases structName fields,
      indent 2 "}",
      indent 1 "}",
      indent 2 "Err(ApiError::ConvexClientError(\"Expected a string for " ++ structName ++ "\".to_string()))",
      indent 1 "}",
      indent 1 "}",
      "",
      indent 1 $ "impl FromConvexValue for " ++ structName ++ " {",
      indent 2 $ "fn from_convex(value: Value) -> Result<Self, ApiError> {",
      indent 3 $ structName ++ "::try_from(value)",
      indent 2 $ "}",
      indent 1 $ "}",
      ""
    ]

generateEnumMatchCases :: String -> [Schema.ConvexType] -> [String]
generateEnumMatchCases structName fields =
  let cases =
        map
          ( \case
              (Schema.VLiteral s) -> "\"" ++ s ++ "\" => Ok(" ++ structName ++ "::" ++ toPascalCase s ++ "),"
              _ -> error "Expected a literal for enum field"
          )
          fields
   in cases ++ ["x => Err(ApiError::ConvexClientError(\"Expected one of " ++ enumValues ++ " for " ++ structName ++ " but got \".to_string() + &x.to_string()))"]
  where
    enumValues = "[" ++ (intercalate ", " $ map Schema.getLiteralString fields) ++ "]"

-- #[derive(Default, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
-- pub enum AccessLevelEnum {
--     #[default]
--     #[serde(rename = "read")]
--     Read,
--     #[serde(rename = "edit")]
--     Edit,
-- }
--
-- impl From<Value> for AccessLevelEnum {
--     fn from(value: Value) -> Self {
--         match value {
--             Value::String(s) => match s.as_str() {
--                 "read" => AccessLevelEnum::Read,
--                 "edit" => AccessLevelEnum::Edit,
--                 _ => panic!("Unknown AccessLevelEnum value"),
--             },
--             _ => panic!("Expected a string for AccessLevelEnum"),
--         }
--     }
-- }

generateFromConvexValueImpl :: String -> [(String, Schema.ConvexType)] -> String
generateFromConvexValueImpl structName fields =
  unlines
    [ "impl TryFrom<Value> for " ++ structName ++ " {",
      indent 1 "type Error = ApiError;",
      indent 1 "fn try_from(value: Value) -> Result<Self, Self::Error> {",
      indent 2 "let obj = match value {",
      indent 3 "Value::Object(map) => map,",
      indent 3 "_ => return Err(ApiError::ConvexClientError(\"Expected object\".to_string())),",
      indent 2 "};",
      unlines $ map (indent 2 . generateAccessorFnFromConvexValue structName) fields,
      unlines $ map (indent 2) $ generateFromConvexValueResult structName fields,
      indent 1 "}",
      "}",
      "",
      indent 1 $ "impl FromConvexValue for " ++ structName ++ " {",
      indent 2 $ "fn from_convex(value: Value) -> Result<Self, ApiError> {",
      indent 3 $ structName ++ "::try_from(value)",
      indent 2 $ "}",
      indent 1 $ "}",
      ""
    ]

generateFromConvexValueResult :: String -> [(String, Schema.ConvexType)] -> [String]
generateFromConvexValueResult structName fields =
  let fieldNames = map (toSnakeCase . fst) fields
      fieldAccessors = map (\name -> "get_" ++ name) fieldNames
      setterStatements =
        zipWith
          ( \name getter -> case name of
              "_creation_time" -> indent 3 $ "_creation_time: " ++ getter ++ "(&obj, \"_creationTime\")?"
              n -> indent 3 $ n ++ ": " ++ getter ++ "(&obj, \"" ++ n ++ "\")?"
          )
          fieldNames
          fieldAccessors
   in [ indent 2 ("Ok(" ++ structName ++ " {"),
        indent 3 $ intercalate ",\n" $ setterStatements,
        indent 2 "})"
      ]

--        Ok(types::GetAssetsReturnObject {
--            _id: Id::new(get__id(&obj, "_id")?),
--            _creation_time: get__creation_time(&obj, "_creationTime")?,
--            project_id: Id::new(get_project_id(&obj, "project_id")?),
--            asset_name: get_asset_name(&obj, "asset_name")?,
--            asset_essence_mtime: get_asset_essence_mtime(&obj, "asset_essence_mtime")?,
--            link_metadata: get_link_metadata(&obj, "link_metadata")?,
--        })

generateAccessorFnFromConvexValue :: String -> (String, Schema.ConvexType) -> String
generateAccessorFnFromConvexValue structName (fieldName, Schema.VOptional fieldType) =
  let fieldNameSnake = toSnakeCase fieldName
      getterName = "get_" ++ fieldNameSnake
      (fieldTypeStr, _) = case stripPrefix (reverse "Object") (reverse structName) of
        Just cleanStructName -> toRustType (reverse cleanStructName ++ capitalize fieldName) fieldType
        Nothing -> error "Expected struct name to end with 'Object' for optional field"
   in unlines
        [ indent 0 $ "fn " ++ getterName ++ "(map: &BTreeMap<String, Value>, key: &str) -> Result<Option<" ++ fieldTypeStr ++ ">, ApiError> {",
          indent 1 $ "match map.get(key) {",
          indent 2 $ "Some(v) => {",
          indent 3 $ "Ok(Some(FromConvexValue::from_convex(v.clone())?))",
          indent 2 $ "}",
          indent 2 $ "_ => Ok(None),",
          indent 1 $ "}",
          indent 0 $ "}",
          ""
        ]
generateAccessorFnFromConvexValue structName (fieldName, fieldType) =
  let fieldNameSnake = toSnakeCase fieldName
      getterName = "get_" ++ fieldNameSnake
      (fieldTypeStr, _) = case stripPrefix (reverse "Object") (reverse structName) of
        Just cleanStructName -> toRustType (reverse cleanStructName ++ capitalize fieldName) fieldType
        Nothing -> error "Expected struct name to end with 'Object' for optional field"
   in unlines
        [ indent 0 $ "fn " ++ getterName ++ "(map: &BTreeMap<String, Value>, key: &str) -> Result<" ++ fieldTypeStr ++ ", ApiError> {",
          indent 1 $ "match map.get(key) {",
          indent 2 $ "Some(v) => {",
          indent 3 $ "Ok(FromConvexValue::from_convex(v.clone())?)",
          indent 2 $ "}",
          indent 2 $ "_ => return Err(ApiError::ConvexClientError(format!(\"Expected field (" ++ fieldTypeStr ++ ") '{}' not found\", key))),",
          indent 1 $ "}",
          indent 0 $ "}",
          ""
        ]

-- An example implementation:
--
-- pub struct GetAssetsReturnObject {
--     #[serde(default)]
--     pub _id: Id<types::AssetsDoc>,
--     #[serde(rename = "_creationTime")]
--     #[serde(default)]
--     pub _creation_time: f64,
--     #[serde(default)]
--     pub project_id: Id<types::ProjectsDoc>,
--     #[serde(default)]
--     pub asset_name: String,
--     #[serde(default)]
--     pub link_metadata: types::GetAssetsReturnLinkMetadataObject,
--     #[serde(default)]
--     pub asset_essence_mtime: i64,
-- }
--
-- impl TryFrom<Value> for types::GetAssetsReturnObject {
--    type Error = ApiError;
--
--    fn try_from(value: Value) -> Result<Self, Self::Error> {
--        let obj = match value {
--            Value::Object(map) => map,
--            _ => return Err(ApiError::ConvexClientError("Expected object".to_string())),
--        };
--
--        fn get__id(map: &BTreeMap<String, Value>, key: &str) -> Result<String, ApiError> {
--            match map.get(key) {
--                Some(Value::String(s)) => Ok(s.clone()),
--                _ => Err(ApiError::ConvexClientError(format!(
--                    "Expected string for field '{}'",
--                    key
--                ))),
--            }
--        }
--
--        fn get_project_id(map: &BTreeMap<String, Value>, key: &str) -> Result<String, ApiError> {
--            match map.get(key) {
--                Some(Value::String(s)) => Ok(s.clone()),
--                _ => Err(ApiError::ConvexClientError(format!(
--                    "Expected string for field '{}'",
--                    key
--                ))),
--            }
--        }
--
--        fn get__creation_time(map: &BTreeMap<String, Value>, key: &str) -> Result<f64, ApiError> {
--            match map.get(key) {
--                Some(Value::Float64(f)) => Ok(*f),
--                _ => Err(ApiError::ConvexClientError(format!(
--                    "Expected float64 for field '{}'",
--                    key
--                ))),
--            }
--        }
--
--        fn get_asset_essence_mtime(
--            map: &BTreeMap<String, Value>,
--            key: &str,
--        ) -> Result<i64, ApiError> {
--            match map.get(key) {
--                Some(Value::Int64(i)) => Ok(*i),
--                _ => Err(ApiError::ConvexClientError(format!(
--                    "Expected int64 for field '{}'",
--                    key
--                ))),
--            }
--        }
--
--        fn get_asset_name(map: &BTreeMap<String, Value>, key: &str) -> Result<String, ApiError> {
--            match map.get(key) {
--                Some(Value::String(s)) => Ok(s.clone()),
--                _ => Err(ApiError::ConvexClientError(format!(
--                    "Expected string for field '{}'",
--                    key
--                ))),
--            }
--        }
--
--        fn get_link_metadata(
--            map: &BTreeMap<String, Value>,
--            key: &str,
--        ) -> Result<types::GetAssetsReturnLinkMetadataObject, ApiError> {
--            match map.get(key) {
--                Some(Value::Object(inner)) => {
--                    let length = match inner.get("length") {
--                        Some(Value::Int64(i)) => *i,
--                        _ => {
--                            return Err(ApiError::ConvexClientError(
--                                "Expected int64 for 'length'".into(),
--                            ));
--                        }
--                    };
--                    let sample_rate = match inner.get("sample_rate") {
--                        Some(Value::Int64(i)) => *i,
--                        _ => {
--                            return Err(ApiError::ConvexClientError(
--                                "Expected int64 for 'sample_rate'".into(),
--                            ));
--                        }
--                    };
--                    let summary = match inner.get("summary") {
--                        Some(Value::Bytes(b)) => b.clone(),
--                        _ => {
--                            return Err(ApiError::ConvexClientError(
--                                "Expected bytes for 'summary'".into(),
--                            ));
--                        }
--                    };
--                    Ok(types::GetAssetsReturnLinkMetadataObject {
--                        length,
--                        sample_rate,
--                        summary,
--                    })
--                }
--                _ => Err(ApiError::ConvexClientError(
--                    "Expected object for 'link_metadata'".into(),
--                )),
--            }
--        }
--
--        Ok(types::GetAssetsReturnObject {
--            _id: Id::new(get__id(&obj, "_id")?),
--            _creation_time: get__creation_time(&obj, "_creationTime")?,
--            project_id: Id::new(get_project_id(&obj, "project_id")?),
--            asset_name: get_asset_name(&obj, "asset_name")?,
--            asset_essence_mtime: get_asset_essence_mtime(&obj, "asset_essence_mtime")?,
--            link_metadata: get_link_metadata(&obj, "link_metadata")?,
--        })
--    }
-- }

-- Some(Value::Array(v)) => Ok(Some(
--     v.iter()
--         .map(|item| item.clone().try_into())
--         .collect::<Result<Vec<_>, ApiError>>()?,
-- )),

generateFieldToConvexValue :: (String, Schema.ConvexType) -> String
generateFieldToConvexValue (fieldName, Schema.VOptional inner) =
  let fieldNameSnake = toSnakeCase fieldName
      -- `v` is the unwrapped value from the Option.
      valueExpr = innerValueToConvexOptional "v" inner
   in unlines
        [ "if let Some(v) = &self." ++ fieldNameSnake ++ " {",
          indent 1 ("btmap.insert(\"" ++ fieldName ++ "\".to_string(), " ++ valueExpr ++ ");"),
          "}"
        ]
generateFieldToConvexValue (fieldName, fieldType) =
  let fieldNameSnake = toSnakeCase fieldName
      valueExpr = innerValueToConvexNonOptional ("self." ++ fieldNameSnake) fieldType
   in "btmap.insert(\"" ++ fieldName ++ "\".to_string(), " ++ valueExpr ++ ");"

-- | Generates the conversion for a non-optional inner value.
innerValueToConvexOptional :: String -> Schema.ConvexType -> String
innerValueToConvexOptional varName (Schema.VArray inner) =
  let itemConversion = innerValueToConvexArray "item" inner
      -- We can check `isFallible` by looking for `Value::try_from` or `?` in the conversion.
      isFallible = "?" `isInfixOf` itemConversion || "Value::try_from" `isInfixOf` itemConversion || "to_convex_value" `isInfixOf` itemConversion
   in if isFallible
        then "Value::Array(" ++ varName ++ ".iter().map(|item| " ++ itemConversion ++ ").collect::<Result<Vec<_>, _>>()?)"
        else "Value::Array(" ++ varName ++ ".iter().map(|item| " ++ itemConversion ++ ").collect())"
innerValueToConvexOptional varName (Schema.VObject _) =
  varName ++ ".to_convex_value()"
innerValueToConvexOptional varName t
  | isComplexForBTreeMap t = "Value::try_from(serde_json::to_value(" ++ toClonedValue varName t ++ ").unwrap_or(\"unable to serialize\".into()))?"
  | otherwise = "Value::from(" ++ toClonedValue varName t ++ ")"

innerValueToConvexArray :: String -> Schema.ConvexType -> String
innerValueToConvexArray varName (Schema.VArray inner) =
  let itemConversion = innerValueToConvexArray "item" inner
      isFallible = "?" `isInfixOf` itemConversion || "Value::try_from" `isInfixOf` itemConversion || "to_convex_value" `isInfixOf` itemConversion
   in if isFallible
        then "Value::Array(" ++ varName ++ ".iter().map(|item| " ++ itemConversion ++ ").collect::<Result<Vec<_>, _>>()?)"
        else "Value::Array(" ++ varName ++ ".iter().map(|item| " ++ itemConversion ++ ").collect())"
innerValueToConvexArray varName (Schema.VObject _) =
  varName ++ ".to_convex_value()"
innerValueToConvexArray varName t
  | isComplexForBTreeMap t = "Value::try_from(serde_json::to_value(" ++ toClonedValue varName t ++ ").unwrap_or(\"unable to serialize\".into()))"
  | otherwise = "Value::from(" ++ toClonedValue varName t ++ ")"

innerValueToConvexNonOptional :: String -> Schema.ConvexType -> String
innerValueToConvexNonOptional varName (Schema.VArray inner) =
  let itemConversion = innerValueToConvexArray "item" inner
      isFallible = "?" `isInfixOf` itemConversion || "Value::try_from" `isInfixOf` itemConversion || "to_convex_value" `isInfixOf` itemConversion
   in if isFallible
        then "Value::Array(" ++ varName ++ ".iter().map(|item| " ++ itemConversion ++ ").collect::<Result<Vec<_>, _>>()?)"
        else "Value::Array(" ++ varName ++ ".iter().map(|item| " ++ itemConversion ++ ").collect())"
innerValueToConvexNonOptional varName (Schema.VBytes) = "Value::from(" ++ varName ++ ".to_vec())"
innerValueToConvexNonOptional varName (Schema.VObject _) = "Value::from(" ++ varName ++ ".to_convex_value()?)"
innerValueToConvexNonOptional varName t
  | isComplexForBTreeMap t = "Value::try_from(serde_json::to_value(" ++ toClonedValue varName t ++ ").unwrap_or(\"unable to serialize\".into()))?"
  | otherwise = "Value::from(" ++ toClonedValueNonOptional varName t ++ ")"

toClonedValueNonOptional :: String -> Schema.ConvexType -> String
toClonedValueNonOptional varName (Schema.VString) = varName ++ ".to_string()"
toClonedValueNonOptional varName t
  | isPassedByCopy t = varName
  | otherwise = varName ++ ".clone()"

isComplexForBTreeMap :: Schema.ConvexType -> Bool
isComplexForBTreeMap (Schema.VUnion _) = True
isComplexForBTreeMap (Schema.VReference _) = True
isComplexForBTreeMap Schema.VAny = True
isComplexForBTreeMap _ = False

toRustType :: String -> Schema.ConvexType -> (String, [String])
toRustType nameHint typ = case typ of
  Schema.VString -> ("String", [])
  Schema.VNumber -> ("f64", [])
  Schema.VInt64 -> ("i64", [])
  Schema.VFloat64 -> ("f64", [])
  Schema.VBoolean -> ("bool", [])
  Schema.VAny -> ("serde_json::Value", [])
  Schema.VNull -> ("()", [])
  Schema.VId t -> ("Id<types::" ++ toPascalCase t ++ "Doc>", [])
  Schema.VBytes -> ("Vec<u8>", [])
  Schema.VArray inner ->
    let (innerType, nested) = toRustType nameHint inner
     in ("Vec<" ++ innerType ++ ">", nested)
  Schema.VOptional inner ->
    let (innerType, nested) = toRustType nameHint inner
     in ("Option<" ++ innerType ++ ">", nested)
  Schema.VObject fields ->
    let className = toPascalCase nameHint ++ "Object"
        (fieldLines, nestedFields) = unzip $ map (generateField nameHint) (map (\(n, t) -> Schema.Field n t) fields)
        implBlock = generateToConvexValueImpl (className) fields
        fromBlock = generateFromConvexValueImpl (className) fields
        newModel =
          unlines
            [ indent 1 "#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq)]",
              indent 1 ("pub struct " ++ className ++ " {"),
              unlines fieldLines,
              indent 1 "}",
              "",
              implBlock,
              "",
              fromBlock
            ]
     in ("types::" ++ className, concat nestedFields ++ [newModel])
  Schema.VUnion types ->
    let nonNullTypes = filter (/= Schema.VNull) types
     in case nonNullTypes of
          [] -> ("Option<()>", [])
          [singleType] ->
            let (innerType, nested) = toRustType nameHint singleType
             in ("Option<" ++ innerType ++ ">", nested)
          _ ->
            if all Schema.isLiteral nonNullTypes && not (null nonNullTypes)
              then
                let enumName = toPascalCase nameHint
                    variantNames = map Schema.getLiteralString nonNullTypes
                    buildVariantLines [] = []
                    buildVariantLines (first : rest) =
                      (indent 2 "#[default]\n" ++ indent 2 ("#[serde(rename = \"" ++ first ++ "\")]\n") ++ indent 2 (toPascalCase first ++ ","))
                        : map (\v -> indent 2 ("#[serde(rename = \"" ++ v ++ "\")]\n") ++ indent 2 (toPascalCase v ++ ",")) rest
                    newEnum =
                      unlines
                        [ indent 1 "#[derive(Default, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]",
                          indent 1 ("pub enum " ++ enumName ++ " {"),
                          unlines $ buildVariantLines variantNames,
                          indent 1 "}"
                        ]
                 in ("types::" ++ enumName, [newEnum])
              else ("serde_json::Value", []) -- Fallback for complex unions
  Schema.VLiteral _ -> (toPascalCase nameHint, [])
  Schema.VReference n -> ("types::" ++ toPascalCase n, [])
  Schema.VVoid -> ("()", [])

toRustBorrowType :: String -> String
toRustBorrowType rustType
  | rustType == "String" = "&str"
  | rustType == "serde_json::Value" = "&serde_json::Value"
  | rustType == "Vec<u8>" = "&[u8]"
  | "Id<" `isPrefixOf` rustType = "&" ++ rustType
  | "Vec<" `isPrefixOf` rustType = let inner = take (length rustType - 5) (drop 4 rustType) in "&[" ++ inner ++ "]"
  | "Option<" `isPrefixOf` rustType = let inner = take (length rustType - 8) (drop 7 rustType) in "Option<" ++ toRustBorrowType inner ++ ">"
  | otherwise = rustType

needsOptionalWrapper :: Schema.ConvexType -> Bool
needsOptionalWrapper (Schema.VOptional _) = True
needsOptionalWrapper (Schema.VUnion ts) = Schema.VNull `elem` ts
needsOptionalWrapper _ = False

toPascalCase :: String -> String
toPascalCase s = concatMap capitalize parts
  where
    parts = words $ map (\c -> if c == '_' then ' ' else c) s

capitalize :: String -> String
capitalize "" = ""
capitalize (c : cs) = toUpper c : cs

toSnakeCase :: String -> String
toSnakeCase "" = ""
toSnakeCase (c : cs) = toLower c : go cs
  where
    go (c' : cs')
      | isUpper c' = '_' : toLower c' : go cs'
      | otherwise = c' : go cs'
    go "" = ""

stripNewlines :: String -> String
stripNewlines s = unlines . filter (/= "") $ lines s