convex-schema-parser 0.1.4.0 → 0.1.5.0
raw patch · 4 files changed
+142/−80 lines, 4 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Backend.Python: instance GHC.Classes.Eq Backend.Python.Definition
+ Backend.Python: instance GHC.Classes.Ord Backend.Python.Definition
+ Backend.Python: instance GHC.Show.Show Backend.Python.Definition
Files
- CHANGELOG.md +10/−0
- convex-schema-parser.cabal +1/−1
- src/Backend/Python.hs +126/−78
- src/Backend/Rust.hs +5/−1
CHANGELOG.md view
@@ -1,5 +1,14 @@ # Revision history for convex-schema-parser +## 0.1.5.0 -- 2025-07-12++* **Backend Improvements:**+ * **Python:**+ * The generated Python code now correctly orders class and constant definitions based on their dependencies, resolving issues with out-of-order declarations.+ * Added support for `ConvexInt64` with Pydantic, ensuring proper validation and serialization.+ * **Rust:**+ * Generated `TryFrom` implementations for anonymous enums, improving type safety and ergonomics.+ ## 0.1.4.0 -- 2025-07-09 * **Fixes:**@@ -41,6 +50,7 @@ * Normalized generated strings for easier testing. * Namespaced functions to avoid conflicts in the Python and Rust backends. * Fixed a bug where empty function arguments were not being properly handled in the action parser.+ * Properly unify nested types during the unification pass. * **Miscellaneous:** * Updated README and LICENSE.
convex-schema-parser.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: convex-schema-parser-version: 0.1.4.0+version: 0.1.5.0 license: MIT author: Norbert Dzikowski maintainer: lambdax.one@icloud.com
src/Backend/Python.hs view
@@ -6,29 +6,53 @@ import qualified Convex.Parser as P import qualified Convex.Schema.Parser as Schema import Data.Char (isUpper, toLower, toUpper)-import Data.List (intercalate, isPrefixOf, nub)+import Data.List (intercalate, isPrefixOf, nub, partition) import qualified Data.Map.Strict as Map import Data.Maybe (catMaybes)+import qualified Data.Set as Set import PathTree +-- | Represents a generated Python definition (e.g., a class or a constant).+data Definition+ = Definition+ { defName :: String,+ defDeps :: Set.Set String,+ defCode :: String+ }+ deriving (Show, Eq, Ord)++-- | Sorts definitions topologically based on their dependencies.+-- A simple implementation that iteratively finds definitions with no remaining dependencies.+topologicalSort :: [Definition] -> [Definition]+topologicalSort defs = go defs [] (Set.fromList $ map defName defs)+ where+ go [] sorted _ = sorted+ go remaining sorted definedNames =+ let (ready, pending) = partition (\d -> Set.null (defDeps d `Set.intersection` definedNames)) remaining+ in if null ready && not (null pending)+ then error ("Circular dependency detected in definitions: " ++ show (map defName pending))+ else+ let newSorted = sorted ++ ready+ newDefinedNames = definedNames `Set.difference` (Set.fromList $ map defName ready)+ in go pending newSorted newDefinedNames+ -- Helper function to prepend a given number of spaces (4 per level). indent :: Int -> String -> String indent n s = replicate (n * 4) ' ' ++ s generatePythonCode :: P.ParsedProject -> String generatePythonCode project =- let (constantsCode, nestedModelsFromConstants) = generateAllConstants (P.ppConstants project)- (schemaCode, nestedFromFields) = generateAllTables (P.ppSchema project)- (apiClassCode, nestedFromFuncs) = generateApiClass (P.ppFunctions project)- allNestedCode = unlines . nub $ nestedFromFields ++ nestedModelsFromConstants ++ nestedFromFuncs+ let constantDefs = generateAllConstants (P.ppConstants project)+ tableDefs = generateAllTables (P.ppSchema project)+ (apiDef, apiNestedDefs) = generateApiClass (P.ppFunctions project)+ allDefs = nub $ constantDefs ++ tableDefs ++ apiNestedDefs ++ [apiDef]+ sortedDefs = topologicalSort allDefs+ definitionsCode = unlines $ map defCode sortedDefs aliasesCode = generateAliases (P.ppSchema project) in unlines [ generateHeader,- constantsCode,- allNestedCode,- schemaCode,- aliasesCode,- apiClassCode+ definitionsCode,+ aliasesCode ] -- | Generates the static header for the Python file.@@ -37,11 +61,25 @@ unlines [ "from typing import Any, Generic, Iterator, Literal, TypeVar", "",- "from convex import ConvexClient",+ "from convex import ConvexClient, ConvexInt64", "from pydantic import BaseModel, Field, TypeAdapter, ValidationError", "from pydantic_core import core_schema", "", "",+ "class PydanticConvexInt64(ConvexInt64):",+ " @classmethod",+ " def __get_pydantic_core_schema__(cls, s, h) -> core_schema.CoreSchema:",+ " from_int_schema = core_schema.no_info_after_validator_function(cls, core_schema.int_schema())",+ "",+ " def validate_from_instance(v):",+ " return PydanticConvexInt64(v.value)",+ "",+ " from_instance_schema = core_schema.no_info_after_validator_function(",+ " validate_from_instance, core_schema.is_instance_schema(ConvexInt64)",+ " )",+ "",+ " return core_schema.union_schema([from_instance_schema, from_int_schema])",+ "", "T = TypeVar('T')", "class Id(str, Generic[T]):", " @classmethod",@@ -51,30 +89,32 @@ ] -- | Generates Python type aliases for all the named constants.-generateAllConstants :: Map.Map String Schema.ConvexType -> (String, [String])+generateAllConstants :: Map.Map String Schema.ConvexType -> [Definition] generateAllConstants constants =- let results = map (generateConstant . fst) (Map.toList constants)- in (unlines $ map fst results, concatMap snd results)+ concatMap (generateConstant . fst) (Map.toList constants) where+ generateConstant :: String -> [Definition] generateConstant name = let constType = constants Map.! name- (pyType, _, _, nested) = toPythonTypeParts name constType- in (name ++ " = " ++ pyType, nested)+ (pyType, _, _, nestedDefs, deps) = toPythonTypeParts name constType+ code = name ++ " = " ++ pyType+ definition = Definition {defName = name, defDeps = deps, defCode = code}+ in definition : nestedDefs -- | Generates Pydantic BaseModel classes for all tables.-generateAllTables :: Schema.Schema -> (String, [String])+generateAllTables :: Schema.Schema -> [Definition] generateAllTables (Schema.Schema tables) =- let (tableCodes, nested) = unzip $ map generateTable tables- in (unlines tableCodes, concat nested)+ let (tableDefs, nestedDefs) = unzip $ map generateTable tables+ in tableDefs ++ concat nestedDefs -- | Generates a single Pydantic BaseModel class for a table.-generateTable :: Schema.Table -> (String, [String])+generateTable :: Schema.Table -> (Definition, [Definition]) generateTable table = let className = toClassName (Schema.tableName table) idField = Schema.Field "_id" (Schema.VId (Schema.tableName table)) creationTimeField = Schema.Field "_creationTime" Schema.VNumber allFields = [idField, creationTimeField] ++ Schema.tableFields table- (fieldLines, nestedModelsFromFields) = unzip $ map (generateField className) allFields+ (fieldLines, nestedDefsFromFields, fieldDeps) = unzip3 $ map (generateField className) allFields tableCode = unlines [ "class " ++ className ++ "(BaseModel):",@@ -83,7 +123,9 @@ indent 1 "class Config:", indent 2 "populate_by_name: bool = True" ]- in (tableCode, concat nestedModelsFromFields)+ deps = Set.delete className (Set.unions fieldDeps)+ definition = Definition {defName = className, defDeps = deps, defCode = tableCode}+ in (definition, concat nestedDefsFromFields) -- | Generates singular type aliases for all table documents. generateAliases :: Schema.Schema -> String@@ -94,12 +136,12 @@ toAlias t = toSingular (Schema.tableName t) ++ " = " ++ toClassName (Schema.tableName t) -- | Generates the code for a single Python function wrapper.-generateFunction :: Int -> Action.ConvexFunction -> (String, [String])+generateFunction :: Int -> Action.ConvexFunction -> (String, [Definition], Set.Set String) generateFunction level func = let funcName = Action.funcName func- (argSignature, payloadMapping, nestedFromArgs) = generateArgSignature funcName (Action.funcArgs func)+ (argSignature, payloadMapping, defsFromArgs, depsFromArgs) = generateArgSignature funcName (Action.funcArgs func) funcNameSnake = toSnakeCase funcName- (rawReturnHint, isModelReturn, nestedFromReturn) = getReturnType funcName (Action.funcReturn func)+ (rawReturnHint, isModelReturn, defsFromReturn, depsFromReturn) = getReturnType funcName (Action.funcReturn func) handlerCall = case Action.funcType func of Action.Query -> "self._client.query"@@ -152,14 +194,14 @@ indent (level + 2) ("print(f\"Error in '" ++ funcNameSnake ++ "': {e}\")"), indent (level + 2) "raise" ]- in (funcCode, nestedFromArgs ++ nestedFromReturn)+ in (funcCode, defsFromArgs ++ defsFromReturn, depsFromArgs `Set.union` depsFromReturn) -generateSubscriptionFunction :: Int -> Action.ConvexFunction -> (String, [String])+generateSubscriptionFunction :: Int -> Action.ConvexFunction -> (String, [Definition], Set.Set String) generateSubscriptionFunction level func = let funcName = Action.funcName func- (argSignature, payloadMapping, nestedFromArgs) = generateArgSignature funcName (Action.funcArgs func)+ (argSignature, payloadMapping, defsFromArgs, depsFromArgs) = generateArgSignature funcName (Action.funcArgs func) funcNameSnake = "subscribe_" ++ toSnakeCase funcName- (returnHint, _, nestedFromReturn) = getReturnType funcName (Action.funcReturn func)+ (returnHint, _, defsFromReturn, depsFromReturn) = getReturnType funcName (Action.funcReturn func) finalReturnHint = "Iterator[" ++ returnHint ++ "]" fullFuncPath = "\"" ++ Action.funcPath func ++ ":" ++ funcName ++ "\"" @@ -181,25 +223,25 @@ indent (level + 3) "print(f\"Validation error in subscription update: {e}\")", indent (level + 3) "continue" ]- in (funcCode, nestedFromArgs ++ nestedFromReturn)+ in (funcCode, defsFromArgs ++ defsFromReturn, depsFromArgs `Set.union` depsFromReturn) -generateApiStructure :: Int -> PathTree -> ([String], [String], [String])+generateApiStructure :: Int -> PathTree -> ([String], [String], [Definition], Set.Set String) generateApiStructure level (DirNode dir) =- let (inits, defs, nested) = foldl processEntry ([], [], []) (Map.toList dir)- in (inits, defs, nested)+ let (inits, defs, nestedDefs, deps) = foldl processEntry ([], [], [], Set.empty) (Map.toList dir)+ in (inits, defs, nestedDefs, deps) where- processEntry (is, ds, ns) (_name, FuncNode func) =- let (funcDef, nestedFromFunc) = generateFunction level func- (subDef, nestedFromSub) =+ processEntry (is, ds, nds, ds_deps) (_name, FuncNode func) =+ let (funcDef, defsFromFunc, depsFromFunc) = generateFunction level func+ (subDef, defsFromSub, depsFromSub) = if Action.funcType func == Action.Query then generateSubscriptionFunction level func- else ("", [])- in (is, ds ++ [funcDef, subDef], ns ++ nestedFromFunc ++ nestedFromSub)- processEntry (is, ds, ns) (name, DirNode subDir) =+ else ("", [], Set.empty)+ in (is, ds ++ [funcDef, subDef], nds ++ defsFromFunc ++ defsFromSub, ds_deps `Set.union` depsFromFunc `Set.union` depsFromSub)+ processEntry (is, ds, nds, ds_deps) (name, DirNode subDir) = let className = capitalize name attrName = toSnakeCase name initLine = "self." ++ attrName ++ " = self." ++ className ++ "(self._client)"- (subInits, subDefs, nestedFromSub) = generateApiStructure (level + 1) (DirNode subDir)+ (subInits, subDefs, defsFromSub, depsFromSub) = generateApiStructure (level + 1) (DirNode subDir) classDef = unlines $ [ "",@@ -209,13 +251,13 @@ ] ++ map (indent (level + 2)) subInits ++ subDefs- in (is ++ [initLine], ds ++ [classDef], ns ++ nestedFromSub)-generateApiStructure _ (FuncNode _) = ([], [], [])+ in (is ++ [initLine], ds ++ [classDef], nds ++ defsFromSub, ds_deps `Set.union` depsFromSub)+generateApiStructure _ (FuncNode _) = ([], [], [], Set.empty) -generateApiClass :: [Action.ConvexFunction] -> (String, [String])+generateApiClass :: [Action.ConvexFunction] -> (Definition, [Definition]) generateApiClass funcs = let tree = buildPathTree funcs- (initLines, definitionLines, nestedModels) = generateApiStructure 1 tree+ (initLines, definitionLines, nestedDefs, deps) = generateApiStructure 1 tree header = [ "\n# --- API Client Class ---\n", "class API:",@@ -224,36 +266,39 @@ indent 2 "self._client = client" ] body = map (indent 2) initLines ++ definitionLines- in (unlines (header ++ body), nub nestedModels)+ apiCode = unlines (header ++ body)+ apiDef = Definition {defName = "API", defDeps = deps, defCode = apiCode}+ in (apiDef, nestedDefs) -- Helper to generate Python function arguments and the payload dictionary mapping.-generateArgSignature :: String -> [(String, Schema.ConvexType)] -> (String, String, [String])+generateArgSignature :: String -> [(String, Schema.ConvexType)] -> (String, String, [Definition], Set.Set String) generateArgSignature funcName args = let results = map (\(n, t) -> (n, toPythonTypeParts (capitalize funcName ++ capitalize n) t)) args- sigParts = map (\(n, (t, _, _, _)) -> toSnakeCase n ++ ": " ++ t) results+ sigParts = map (\(n, (t, _, _, _, _)) -> toSnakeCase n ++ ": " ++ t) results payloadParts = map (\(n, _) -> "\"" ++ n ++ "\": " ++ toSnakeCase n) results- nestedModels = concatMap (\(_, (_, _, _, n)) -> n) results- in (intercalate ", " sigParts, intercalate ", " payloadParts, nestedModels)+ nestedDefs = concatMap (\(_, (_, _, _, defs, _)) -> defs) results+ deps = Set.unions $ map (\(_, (_, _, _, _, d)) -> d) results+ in (intercalate ", " sigParts, intercalate ", " payloadParts, nestedDefs, deps) -- Helper to get the return type information.-getReturnType :: String -> Schema.ConvexType -> (String, Bool, [String])+getReturnType :: String -> Schema.ConvexType -> (String, Bool, [Definition], Set.Set String) getReturnType funcName rt =- let (pyType, _, _, nested) = toPythonTypeParts (capitalize funcName ++ "Return") rt+ let (pyType, _, _, nestedDefs, deps) = toPythonTypeParts (capitalize funcName ++ "Return") rt isModel = case rt of Schema.VObject _ -> True Schema.VArray (Schema.VObject _) -> True Schema.VReference _ -> True Schema.VArray (Schema.VReference _) -> True _ -> False- in (pyType, isModel, nested)+ in (pyType, isModel, nestedDefs, deps) -- Helper to generate a single field line for a Pydantic model.-generateField :: String -> Schema.Field -> (String, [String])+generateField :: String -> Schema.Field -> (String, [Definition], Set.Set String) generateField parentClassName field = let originalFieldName = Schema.fieldName field isSystemField = "_" `isPrefixOf` originalFieldName fieldNameSnake = if isSystemField then toSnakeCase (tail originalFieldName) else toSnakeCase originalFieldName- (pyType, isOpt, isArr, nested) = toPythonTypeParts (parentClassName ++ capitalize originalFieldName) (Schema.fieldType field)+ (pyType, isOpt, isArr, nested, deps) = toPythonTypeParts (parentClassName ++ capitalize originalFieldName) (Schema.fieldType field) fieldArgs = let defaultArg =@@ -264,37 +309,38 @@ in intercalate ", " (catMaybes [Just defaultArg, aliasArg]) fieldDef = fieldNameSnake ++ ": " ++ pyType ++ " = Field(" ++ fieldArgs ++ ")"- in (indent 1 fieldDef, nested)+ in (indent 1 fieldDef, nested, deps) -- Core recursive function to generate Python types from the AST.-toPythonTypeParts :: String -> Schema.ConvexType -> (String, Bool, Bool, [String])+toPythonTypeParts :: String -> Schema.ConvexType -> (String, Bool, Bool, [Definition], Set.Set String) toPythonTypeParts nameHint typ = case typ of- Schema.VString -> ("str", False, False, [])- Schema.VNumber -> ("float", False, False, [])- Schema.VInt64 -> ("int", False, False, [])- Schema.VFloat64 -> ("float", False, False, [])- Schema.VBoolean -> ("bool", False, False, [])- Schema.VBytes -> ("bytes", False, False, [])- Schema.VAny -> ("Any", False, False, [])- Schema.VNull -> ("None", True, False, [])- Schema.VId t -> ("Id['" ++ toClassName t ++ "']", False, False, [])+ Schema.VString -> ("str", False, False, [], Set.empty)+ Schema.VNumber -> ("float", False, False, [], Set.empty)+ Schema.VInt64 -> ("PydanticConvexInt64", False, False, [], Set.empty)+ Schema.VFloat64 -> ("float", False, False, [], Set.empty)+ Schema.VBoolean -> ("bool", False, False, [], Set.empty)+ Schema.VBytes -> ("bytes", False, False, [], Set.empty)+ Schema.VAny -> ("Any", False, False, [], Set.empty)+ Schema.VNull -> ("None", True, False, [], Set.empty)+ Schema.VId t -> ("Id['" ++ toClassName t ++ "']", False, False, [], Set.singleton (toClassName t)) Schema.VArray inner ->- let (innerType, isOpt, isArr, nested) = toPythonTypeParts nameHint inner- in ("list[" ++ innerType ++ "]", isOpt, isArr, nested)+ let (innerType, isOpt, isArr, nested, deps) = toPythonTypeParts nameHint inner+ in ("list[" ++ innerType ++ "]", isOpt, isArr, nested, deps) Schema.VOptional inner ->- let (innerType, _, innerIsArray, nested) = toPythonTypeParts nameHint inner- in (innerType ++ " | None", True, innerIsArray, nested)+ let (innerType, _, innerIsArray, nested, deps) = toPythonTypeParts nameHint inner+ in (innerType ++ " | None", True, innerIsArray, nested, deps) Schema.VUnion types -> let results = map (toPythonTypeParts nameHint) types- pyTypes = nub $ map (\(t, _, _, _) -> t) results- nested = concatMap (\(_, _, _, n) -> n) results- in (intercalate " | " pyTypes, False, False, nested)- Schema.VLiteral s -> ("Literal[\"" ++ s ++ "\"]", False, False, [])- Schema.VReference n -> (n, False, False, [])+ pyTypes = nub $ map (\(t, _, _, _, _) -> t) results+ nested = concatMap (\(_, _, _, d, _) -> d) results+ deps = Set.unions $ map (\(_, _, _, _, d) -> d) results+ in (intercalate " | " pyTypes, False, False, nested, deps)+ Schema.VLiteral s -> ("Literal[\"" ++ s ++ "\"]", False, False, [], Set.empty)+ Schema.VReference n -> (n, False, False, [], Set.singleton n) Schema.VObject fields -> let className = capitalize nameHint ++ "Object"- (fieldLines, nested) = unzip $ map (generateField className) (map (\(n, t) -> Schema.Field n t) fields)- newModel =+ (fieldLines, nestedDefs, fieldDeps) = unzip3 $ map (generateField className) (map (\(n, t) -> Schema.Field n t) fields)+ newModelCode = unlines $ [ "class " ++ className ++ "(BaseModel):", unlines fieldLines,@@ -302,8 +348,10 @@ indent 1 "class Config:", indent 2 "populate_by_name: bool = True" ]- in (className, False, False, concat nested ++ [newModel])- Schema.VVoid -> ("None", True, False, [])+ deps = Set.unions fieldDeps+ newModelDef = Definition {defName = className, defDeps = deps, defCode = newModelCode}+ in (className, False, False, newModelDef : concat nestedDefs, Set.singleton className)+ Schema.VVoid -> ("None", True, False, [], Set.empty) capitalize :: String -> String capitalize "" = ""
src/Backend/Rust.hs view
@@ -1021,12 +1021,16 @@ 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+ fromBlock = generateFromConvexValueImplEnum ("types::" ++ enumName) nonNullTypes newEnum = unlines [ indent 1 "#[derive(Default, Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]", indent 1 ("pub enum " ++ enumName ++ " {"), unlines $ buildVariantLines variantNames,- indent 1 "}"+ indent 1 "}",+ "",+ fromBlock,+ "" ] in ("types::" ++ enumName, [newEnum]) else ("serde_json::Value", []) -- Fallback for complex unions