convex-schema-parser 0.1.3.0 → 0.1.4.0
raw patch · 9 files changed
+700/−71 lines, 9 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Convex.Parser: parseProjectFromContents :: String -> String -> [(String, String)] -> IO (Either String ParsedProject)
+ Convex.Parser: runUnificationPass :: ParsedProject -> ParsedProject
Files
- CHANGELOG.md +47/−0
- README.md +4/−0
- convex-schema-parser.cabal +20/−1
- src/Backend/Python.hs +1/−0
- src/Backend/Rust.hs +15/−4
- src/Convex/Parser.hs +170/−64
- test/ActionParserTest.hs +203/−1
- test/Main.hs +3/−1
- test/UnificationTest.hs +237/−0
CHANGELOG.md view
@@ -1,5 +1,52 @@ # Revision history for convex-schema-parser +## 0.1.4.0 -- 2025-07-09++* **Fixes:**+ * Properly unify nested types during the unification pass.++## 0.1.3.0 -- 2025-07-08++* **Schema Parsing:**+ * More robust implementation of the schema parser provides better error handling and more accurate parsing.+ * Correctly handles foreign imported types.+ * Support for `i64`, `f64`, `ArrayBuffer`, and `v.bytes()` has been added.++* **Backend Improvements:**+ * **Rust:**+ * Custom `serde` implementation for `convex::Value`.+ * Validator for generated types.+ * Typed subscription streams for Convex subscriptions.+ * **Python:**+ * Validator for generated types.+ * Pydantic annotations to handle fields prefixed with `_`.+ * Typed subscription generators for Convex subscriptions.++* **Developer Experience:**+ * New `init` command to guide users through the setup process.+ * `optparse-applicative` for a better CLI experience.+ * New "dev mode" to automatically regenerate code on file changes.+ * `justfile` to provide a simple way to run common commands.++* **Fixes:**+ * **Rust Backend:**+ * Correctly use the inner optional value when generating functions.+ * Fixed a bug where `innerValueConversion` was not being properly handled for functions.+ * Custom structs now know how to encode themselves to `convex::Value`.+ * **Python Backend:**+ * Removed redundant imports from the generated Python code.+ * Properly indent code with helpers.+ * Reverted to a sync client.+ * **General:**+ * 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.++* **Miscellaneous:**+ * Updated README and LICENSE.+ * Added a CI/CD pipeline for releases.+ * Support for GHC 9.6.7.+ ## 0.1.0 -- 2025-06-30 * First version
README.md view
@@ -14,6 +14,10 @@ ## Installation The easiest way to use `convex-schema-parser` is currently through the Cabal package manager.++> [!NOTE]+> We are on hackage now! So you can simply run `cabal update` (important, this package is a recent addition to Hackage) and then `cabal install convex-schema-parser` to install the tool quickly without cloning.+ We provide prebuilt binaries for `linux` & `macOS` that you can download and run directly, but `macOS` users have to allow the binary to run first since we do not sign it (yet). A `npm` package `@parsonosai/convex-schema-parser` is also on its way and supported as soon as we get code-signing ready, we currently use a placeholder.
convex-schema-parser.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: convex-schema-parser-version: 0.1.3.0+version: 0.1.4.0 license: MIT author: Norbert Dzikowski maintainer: lambdax.one@icloud.com@@ -26,6 +26,24 @@ common warnings ghc-options: -Wall +-- library convex-client+-- import: warnings+-- exposed-modules: Convex.Client.Client+-- , Convex.Client.Types+-- build-depends: base >= 4.18.3 && < 4.21+-- , websockets >= 0.12.7 && < 0.13+-- , wuss >= 1.1.1 && < 1.2+-- , async >= 2.2.4 && < 2.3+-- , aeson >= 2.2.3 && < 2.3+-- , stm >= 2.5.3 && < 2.6+-- , text >= 2.1 && < 2.2+-- , uuid >= 1.3.13 && < 1.4+-- , data-default >= 0.7.1 && < 0.8+-- , mtl >= 2.3.1 && < 2.4+-- , containers >= 0.7 && < 0.8+-- hs-source-dirs: src+-- default-language: Haskell2010+ library convex-schema-parser-lib import: warnings exposed-modules: Convex.Schema.Parser@@ -75,6 +93,7 @@ , ApiParserTest , SchemaParserTest , RustSerializationTest+ , UnificationTest hs-source-dirs: test build-depends: base, HUnit >=1.6.0 && < 1.7,
src/Backend/Python.hs view
@@ -243,6 +243,7 @@ Schema.VObject _ -> True Schema.VArray (Schema.VObject _) -> True Schema.VReference _ -> True+ Schema.VArray (Schema.VReference _) -> True _ -> False in (pyType, isModel, nested)
src/Backend/Rust.hs view
@@ -452,6 +452,13 @@ generateTableStruct table = let className = toPascalCase (Schema.tableName table) ++ "Doc" (fieldLines, nestedFromFields) = unzip $ map (generateField className) (Schema.tableFields table)+ allFields =+ [ ("_id", Schema.VId (toPascalCase (Schema.tableName table))),+ ("_creation_time", Schema.VFloat64)+ ]+ ++ map (\f -> (Schema.fieldName f, Schema.fieldType f)) (Schema.tableFields table)+ fromBlock = generateFromConvexValueImpl (className) allFields+ toBlock = generateToConvexValueImpl (className) allFields in ( unlines [ "#[derive(Default, Serialize, Deserialize, Debug, Clone, PartialEq)]", ("pub struct " ++ className ++ " {"),@@ -459,9 +466,13 @@ indent 1 ("pub _id: Id<" ++ className ++ ">,"), indent 1 "#[serde(default)]", indent 1 "#[serde(rename = \"_creationTime\")]",- indent 1 "pub creation_time: f64,",+ indent 1 "pub _creation_time: f64,", unlines fieldLines,- "}"+ "}",+ "",+ fromBlock,+ "",+ toBlock ], concat nestedFromFields )@@ -723,7 +734,7 @@ 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"+ Nothing -> toRustType (structName ++ capitalize fieldName) fieldType in unlines [ indent 0 $ "fn " ++ getterName ++ "(map: &BTreeMap<String, Value>, key: &str) -> Result<Option<" ++ fieldTypeStr ++ ">, ApiError> {", indent 1 $ "match map.get(key) {",@@ -740,7 +751,7 @@ 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"+ Nothing -> toRustType (structName ++ capitalize fieldName) fieldType in unlines [ indent 0 $ "fn " ++ getterName ++ "(map: &BTreeMap<String, Value>, key: &str) -> Result<" ++ fieldTypeStr ++ ", ApiError> {", indent 1 $ "match map.get(key) {",
src/Convex/Parser.hs view
@@ -1,11 +1,13 @@+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} -module Convex.Parser (parseProject, ParsedProject (..), apiFileParser) where+module Convex.Parser (parseProject, parseProjectFromContents, ParsedProject (..), apiFileParser, runUnificationPass) where import Control.Monad (forM, void) import qualified Convex.Action.Parser as Action import qualified Convex.Schema.Parser as Schema-import Data.List (sort)+import Data.Char (toUpper)+import Data.List (sort, sortOn) import qualified Data.Map as Map import System.FilePath (replaceExtension, (</>)) import Text.Parsec@@ -53,44 +55,45 @@ void $ apiIdentifier -- Consume the alias, we don't need it return path -parseProject :: FilePath -> FilePath -> IO (Either String ParsedProject)-parseProject schemaPath declRootDir = do- -- Parse the source schema file first to get tables and the initial state with constants.- schemaContent <- readFile schemaPath+parseProjectFromContents ::+ String ->+ String ->+ [(String, String)] ->+ IO (Either String ParsedProject)+parseProjectFromContents schemaContent apiFileContent actionContents = do+ -- 1. Parse Schema schemaResult <- Schema.parseSchema schemaContent- case schemaResult of Left err -> return $ Left ("Failed to parse schema.ts: " ++ show err) Right schemaFile -> do- -- Re-construct the initial state for the action parser from the parsed constants. let initialState = Schema.ParserState {Schema.psConstants = Schema.parsedConstants schemaFile} - -- Parse the _generated/api.d.ts file to discover function modules.- let apiFilePath = declRootDir </> "_generated" </> "api.d.ts"- apiFileContent <- readFile apiFilePath- let modulePaths = case parse apiFileParser apiFilePath apiFileContent of- Left _ -> []+ -- Parse API file to get module paths+ let modulePaths = case parse apiFileParser "(api.d.ts)" apiFileContent of+ Left _ -> [] -- Or should this be an error? The original returns [] Right paths -> filter (/= "schema") paths - putStrLn $ "Found " ++ show (length modulePaths) ++ " action modules in: " ++ apiFilePath+ let actionContentMap = Map.fromList actionContents - -- For each discovered module, parse its corresponding .d.ts file.- allFunctions <- fmap concat $ forM modulePaths $ \modulePath -> do- let fullPath = declRootDir </> replaceExtension modulePath ".d.ts"+ -- Parse action files+ allFunctions <- fmap concat . forM modulePaths $ \modulePath -> do let astPath = replaceExtension modulePath ""-- actionContent <- readFile fullPath- actionResult <- runParserT (Action.parseActionFile astPath) initialState fullPath actionContent-- case actionResult of- Left err -> do- putStrLn $ "Failed to parse actions from: " ++ fullPath ++ " | Error: " ++ show err+ case Map.lookup modulePath actionContentMap of+ Nothing -> do+ putStrLn $ "Action content not found for module: " ++ modulePath return []- Right funcs -> do- putStrLn $ "Parsed actions from: " ++ fullPath- putStrLn $ show (length funcs) ++ " functions found"- return funcs+ Just actionContent -> do+ actionResult <- runParserT (Action.parseActionFile astPath) initialState modulePath actionContent+ case actionResult of+ Left err -> do+ putStrLn $ "Failed to parse actions from: " ++ modulePath ++ " | Error: " ++ show err+ return []+ Right funcs -> do+ putStrLn $ "Parsed actions from: " ++ modulePath+ putStrLn $ show (length funcs) ++ " functions found"+ return funcs + -- Construct and unify project let project = ParsedProject { ppSchema = Schema.parsedSchema schemaFile,@@ -98,49 +101,152 @@ ppFunctions = allFunctions } - return $ Right . unifyProjectTypes $ project+ return . Right . runUnificationPass $ project +parseProject :: FilePath -> FilePath -> IO (Either String ParsedProject)+parseProject schemaPath declRootDir = do+ -- Parse the source schema file first to get tables and the initial state with constants.+ schemaContent <- readFile schemaPath+ -- Parse the _generated/api.d.ts file to discover function modules.+ let apiFilePath = declRootDir </> "_generated" </> "api.d.ts"+ apiFileContent <- readFile apiFilePath+ let modulePaths = case parse apiFileParser apiFilePath apiFileContent of+ Left _ -> []+ Right paths -> filter (/= "schema") paths++ putStrLn $ "Found " ++ show (length modulePaths) ++ " action modules in: " ++ apiFilePath++ -- For each discovered module, parse its corresponding .d.ts file.+ actionContents <- forM modulePaths $ \modulePath -> do+ let fullPath = declRootDir </> replaceExtension modulePath ".d.ts"+ content <- readFile fullPath+ return (modulePath, content)++ parseProjectFromContents schemaContent apiFileContent actionContents+ type UnionSignatureMap = Map.Map [String] String --- | Pre-processes the parsed project to replace anonymous unions with named references--- if they structurally match.-unifyProjectTypes :: ParsedProject -> ParsedProject-unifyProjectTypes project =- let unionMap = buildUnionSignatureMap (ppConstants project)- in project {ppFunctions = map (unifyFunctionTypes unionMap) (ppFunctions project)}+type ObjectSignatureMap = Map.Map [(String, Schema.ConvexType)] String++-- | Pre-processes the parsed project to replace anonymous unions and objects+-- with named references if they structurally match. This is done iteratively+-- to a fixed point to handle nested structures.+runUnificationPass :: ParsedProject -> ParsedProject+runUnificationPass project =+ let ephemeralProject = addTableDocsToConstants project+ unifiedProject = go ephemeralProject+ in unifiedProject {ppConstants = ppConstants project} -- Discard ephemeral constants where- buildUnionSignatureMap :: Map.Map String Schema.ConvexType -> UnionSignatureMap- buildUnionSignatureMap constants =- Map.fromList- [ (sort $ map Schema.getLiteralString literals, name)- | (name, Schema.VUnion literals) <- Map.toList constants,- all Schema.isLiteral literals- ]+ go currentProject =+ let nextProject = unifyOnce currentProject+ in if nextProject == currentProject+ then currentProject+ else go nextProject - unifyFunctionTypes :: UnionSignatureMap -> Action.ConvexFunction -> Action.ConvexFunction- unifyFunctionTypes unionMap func =- func- { Action.funcArgs = map (unifyArgType unionMap) (Action.funcArgs func),- Action.funcReturn = unifyTypeRecursively unionMap (Action.funcReturn func)- }+canonicalizeType :: Schema.ConvexType -> Schema.ConvexType+canonicalizeType (Schema.VObject fields) =+ Schema.VObject . sortOn fst . map (\(n, t) -> (n, canonicalizeType t)) $ fields+canonicalizeType (Schema.VUnion types) =+ Schema.VUnion . sort . map canonicalizeType $ types+canonicalizeType (Schema.VArray t) = Schema.VArray (canonicalizeType t)+canonicalizeType (Schema.VOptional t) = Schema.VOptional (canonicalizeType t)+canonicalizeType other = other - unifyArgType :: UnionSignatureMap -> (String, Schema.ConvexType) -> (String, Schema.ConvexType)- unifyArgType unionMap (argName, argType) =- (argName, unifyTypeRecursively unionMap argType)+addTableDocsToConstants :: ParsedProject -> ParsedProject+addTableDocsToConstants project =+ let tableDocs =+ Map.fromList+ [ ( toPascalCase (Schema.tableName table) ++ "Doc",+ Schema.VObject . sortOn fst $+ [("_id", Schema.VId (Schema.tableName table)), ("_creationTime", Schema.VNumber)]+ ++ map+ (\f -> (Schema.fieldName f, Schema.fieldType f))+ (Schema.tableFields table)+ )+ | table <- Schema.getTables (ppSchema project)+ ]+ allConstants = Map.union (ppConstants project) tableDocs+ in project {ppConstants = allConstants} - -- This new recursive function traverses the entire type structure.- unifyTypeRecursively :: UnionSignatureMap -> Schema.ConvexType -> Schema.ConvexType- unifyTypeRecursively unionMap u@(Schema.VUnion literals)+toPascalCase :: String -> String+toPascalCase [] = []+toPascalCase (h : t) = toUpper h : t++buildUnionSignatureMap :: Map.Map String Schema.ConvexType -> UnionSignatureMap+buildUnionSignatureMap constants =+ Map.fromList+ [ (sort $ map Schema.getLiteralString literals, name)+ | (name, Schema.VUnion literals) <- Map.toList constants,+ all Schema.isLiteral literals+ ]++buildObjectSignatureMap :: Map.Map String Schema.ConvexType -> ObjectSignatureMap+buildObjectSignatureMap constants =+ Map.fromList+ [ (sortOn fst fields, name)+ | (name, Schema.VObject fields) <- Map.toList constants+ ]++unifyFunctionTypes :: (Schema.ConvexType -> Schema.ConvexType) -> Action.ConvexFunction -> Action.ConvexFunction+unifyFunctionTypes unifyType func =+ func+ { Action.funcArgs = map (\(name, t) -> (name, unifyType t)) (Action.funcArgs func),+ Action.funcReturn = unifyType (Action.funcReturn func)+ }++unifyTypeRecursively :: Maybe String -> UnionSignatureMap -> ObjectSignatureMap -> Schema.ConvexType -> Schema.ConvexType+unifyTypeRecursively mCurrentName unionMap objectMap = go+ where+ goRec = unifyTypeRecursively Nothing unionMap objectMap++ go u@(Schema.VUnion literals) | all Schema.isLiteral literals = let signature = sort $ map Schema.getLiteralString literals in case Map.lookup signature unionMap of- Just refName -> Schema.VReference refName- Nothing -> u- | otherwise = u- unifyTypeRecursively unionMap (Schema.VObject fields) =- Schema.VObject $ map (\(name, t) -> (name, unifyTypeRecursively unionMap t)) fields- unifyTypeRecursively unionMap (Schema.VArray inner) =- Schema.VArray $ unifyTypeRecursively unionMap inner- unifyTypeRecursively unionMap (Schema.VOptional inner) =- Schema.VOptional $ unifyTypeRecursively unionMap inner- unifyTypeRecursively _ otherType = otherType -- Base cases+ Just refName ->+ if Just refName == mCurrentName+ then canonicalizeType u+ else Schema.VReference refName+ Nothing -> canonicalizeType u+ | otherwise =+ let unifiedUnion = Schema.VUnion (map goRec literals)+ in canonicalizeType unifiedUnion+ go (Schema.VObject fields) =+ let unifiedFields = map (\(name, t) -> (name, goRec t)) fields+ canonicalAttempt = canonicalizeType (Schema.VObject unifiedFields)+ in case canonicalAttempt of+ Schema.VObject signature ->+ let canonicalObject = Schema.VObject signature+ in case Map.lookup signature objectMap of+ Just refName ->+ if Just refName == mCurrentName+ then canonicalObject+ else Schema.VReference refName+ Nothing -> canonicalObject+ _ -> canonicalAttempt -- Should not happen, but safer+ go (Schema.VArray inner) = Schema.VArray (goRec inner)+ go (Schema.VOptional inner) = Schema.VOptional (goRec inner)+ go otherType = otherType++unifyOnce :: ParsedProject -> ParsedProject+unifyOnce project =+ let unionMap = buildUnionSignatureMap (ppConstants project)+ initialObjectMap = buildObjectSignatureMap (ppConstants project)++ -- First, unify the constants themselves. This resolves nested anonymous objects+ -- within the constants first, creating a canonical representation for this pass.+ unifiedConstants =+ Map.mapWithKey (\k -> unifyTypeRecursively (Just k) unionMap initialObjectMap) (ppConstants project)++ -- Now, build the object map for function unification from these *newly unified* constants.+ -- This map contains the canonical object structures for this pass.+ finalObjectMap = buildObjectSignatureMap unifiedConstants++ -- Create the unification function for anonymous types found in function signatures.+ unifyAnonType = unifyTypeRecursively Nothing unionMap finalObjectMap++ unifiedFunctions = map (unifyFunctionTypes unifyAnonType) (ppFunctions project)+ in project+ { ppConstants = unifiedConstants,+ ppFunctions = unifiedFunctions+ }
test/ActionParserTest.hs view
@@ -3,10 +3,65 @@ module ActionParserTest (tests) where import qualified Convex.Action.Parser as Action+import qualified Convex.Parser as Parser import qualified Convex.Schema.Parser as Schema+import qualified Data.Map as Map import Test.HUnit import Text.Parsec (runParserT) +-- A dedicated test runner for unification tests that first parses a schema.+runSchemaUnificationTest ::+ String ->+ String ->+ String ->+ [Action.ConvexFunction] ->+ Test+runSchemaUnificationTest testName schemaString actionString expected =+ testName ~: TestCase $ do+ -- First, parse the schema to get the constants.+ schemaResult <- Schema.parseSchema schemaString+ case schemaResult of+ Left err -> assertFailure ("Schema parser failed: " ++ show err)+ Right parsedSchemaFile -> do+ let initialState = Schema.ParserState {Schema.psConstants = Schema.parsedConstants parsedSchemaFile}+ -- Now, parse the action file with the constants from the schema.+ actionResult <- runParserT (Action.parseActionFile "testPath") initialState "(test)" actionString+ case actionResult of+ Left err -> assertFailure ("Action parser failed: " ++ show err)+ Right funcs ->+ let project =+ Parser.ParsedProject+ { Parser.ppSchema = Schema.parsedSchema parsedSchemaFile,+ Parser.ppConstants = Schema.parsedConstants parsedSchemaFile,+ Parser.ppFunctions = funcs+ }+ unifiedProject = Parser.runUnificationPass project+ in Parser.ppFunctions unifiedProject @?= expected++-- A dedicated test runner for unification tests.+runUnificationTest ::+ String ->+ String ->+ Map.Map String Schema.ConvexType ->+ String ->+ [Action.ConvexFunction] ->+ Test+runUnificationTest testName path constants input expected =+ testName ~: TestCase $ do+ let initialState = Schema.ParserState {Schema.psConstants = constants}+ result <- runParserT (Action.parseActionFile path) initialState "(test)" input+ case result of+ Left err -> assertFailure ("Parser failed: " ++ show err)+ Right funcs ->+ let project =+ Parser.ParsedProject+ { Parser.ppSchema = Schema.Schema {Schema.getTables = []},+ Parser.ppConstants = constants,+ Parser.ppFunctions = funcs+ }+ unifiedProject = Parser.runUnificationPass project+ in Parser.ppFunctions unifiedProject @?= expected+ tests :: Test tests = "Action Definition Parser"@@ -18,7 +73,18 @@ runActionTest "parses internal action with external type as VAny" "functions/stripe" sampleStripeSubscriptionAction expectedStripeSubscriptionAction, runActionTest "parses public action with external type" "functions/stripe" sampleStripeCheckoutAction sampleStripeCheckoutActionExpected, runActionTest "parses public action with external type as VAny" "functions/stripe" sampleStripeSubscriptionActionPublic expectedStripeSubscriptionActionPublic,- runActionTest "parses createAsset mutation with complex args" "functions/assets" sampleCreateAssetAction expectedCreateAssetAction+ runActionTest "parses createAsset mutation with complex args" "functions/assets" sampleCreateAssetAction expectedCreateAssetAction,+ runUnificationTest+ "unifies function return with named doc"+ "functions/users"+ userProfileConstants+ sampleGetUserAction+ expectedGetUserAction,+ runSchemaUnificationTest+ "unifies function return with table doc"+ sampleSchemaWithUserTable+ sampleGetUserTableAction+ expectedGetUserTableAction ] where runActionTest testName path input expected =@@ -204,5 +270,141 @@ ) ], Action.funcReturn = Schema.VId "assets"+ }+ ]++-- Test case for structural unification of a function's return type.+userProfileConstants :: Map.Map String Schema.ConvexType+userProfileConstants =+ Map.fromList+ [ ("UserProfile", userProfileType),+ ("Address", addressType)+ ]+ where+ addressType =+ Schema.VObject+ [ ("street", Schema.VString),+ ("city", Schema.VString)+ ]+ userProfileType =+ Schema.VObject+ [ ("name", Schema.VString),+ ("address", Schema.VReference "Address")+ ]++sampleGetUserAction :: String+sampleGetUserAction =+ unlines+ [ "export declare const getUser: import(\"convex/server\").RegisteredQuery<\"public\", {}, Promise<{",+ " name: string;",+ " address: {",+ " street: string;",+ " city: string;",+ " };",+ "}>>;"+ ]++expectedGetUserAction :: [Action.ConvexFunction]+expectedGetUserAction =+ [ Action.ConvexFunction+ { Action.funcName = "getUser",+ Action.funcPath = "functions/users",+ Action.funcType = Action.Query,+ Action.funcArgs = [],+ Action.funcReturn = Schema.VReference "UserProfile" -- Should be unified+ }+ ]++-- Test case for unification against a table doc.+sampleSchemaWithUserTable :: String+sampleSchemaWithUserTable =+ unlines+ [ "import { defineSchema, defineTable } from \"convex/server\";",+ "import { v } from \"convex/values\";",+ "",+ "export default defineSchema({",+ " users: defineTable({",+ " name: v.string(),",+ " email: v.string()",+ " })",+ "});"+ ]++sampleGetUserTableAction :: String+sampleGetUserTableAction =+ unlines+ [ "export declare const getUser: import(\"convex/server\").RegisteredQuery<\"public\", {}, Promise<{",+ " _id: import(\"convex/values\").GenericId<\"users\">;",+ " _creationTime: number;",+ " name: string;",+ " email: string;",+ "}>>;"+ ]++expectedGetUserTableAction :: [Action.ConvexFunction]+expectedGetUserTableAction =+ [ Action.ConvexFunction+ { Action.funcName = "getUser",+ Action.funcPath = "testPath",+ Action.funcType = Action.Query,+ Action.funcArgs = [],+ Action.funcReturn = Schema.VReference "UsersDoc" -- Should be unified+ }+ ]++sampleSchemaWithAssetsTable :: String+sampleSchemaWithAssetsTable =+ unlines+ [ "import { defineSchema, defineTable } from \"convex/server\";",+ "import { v } from \"convex/values\";",+ "",+ "export default defineSchema({",+ " projects: defineTable({",+ " name: v.string()",+ " }),",+ " assets: defineTable({",+ " project_id: v.id(\"projects\"),",+ " asset_name: v.string(),",+ " asset_essence_mtime: v.number(),",+ " link_metadata: v.object({",+ " sample_rate: v.number(),",+ " summary: v.bytes(),",+ " length: v.number()",+ " })",+ " })",+ "});"+ ]++sampleGetAssetsAction :: String+sampleGetAssetsAction =+ unlines+ [ "export declare const getAssets: import(\"convex/server\").RegisteredQuery<\"public\", {",+ " project_id: import(\"convex/values\").Id<\"projects\">;",+ " secret: string;",+ "}, Promise<Array<{",+ " _id: import(\"convex/values\").Id<\"assets\">;",+ " _creationTime: number;",+ " project_id: import(\"convex/values\").Id<\"projects\">;",+ " asset_name: string;",+ " asset_essence_mtime: number;",+ " link_metadata: {",+ " summary: Uint8Array;",+ " sample_rate: number;",+ " length: number;",+ " };",+ "}>>>;"+ ]++expectedGetAssetsAction :: [Action.ConvexFunction]+expectedGetAssetsAction =+ [ Action.ConvexFunction+ { Action.funcName = "getAssets",+ Action.funcPath = "testPath",+ Action.funcType = Action.Query,+ Action.funcArgs =+ [ ("project_id", Schema.VId "projects"),+ ("secret", Schema.VString)+ ],+ Action.funcReturn = Schema.VArray (Schema.VReference "AssetsDoc") } ]
test/Main.hs view
@@ -4,6 +4,7 @@ import qualified ApiParserTest import qualified RustSerializationTest import qualified SchemaParserTest+import qualified UnificationTest import System.Exit (exitFailure, exitSuccess) import Test.HUnit @@ -15,7 +16,8 @@ [ ApiParserTest.tests, ActionParserTest.tests, SchemaParserTest.tests,- RustSerializationTest.tests+ RustSerializationTest.tests,+ UnificationTest.tests ] -- Run the combined test suite
+ test/UnificationTest.hs view
@@ -0,0 +1,237 @@+{-# LANGUAGE OverloadedStrings #-}++module UnificationTest (tests) where++import qualified Convex.Action.Parser as Action+import qualified Convex.Parser as Parser+import qualified Convex.Schema.Parser as Schema+import Test.HUnit++testUnifyWithTableDoc :: Test+testUnifyWithTableDoc = "unifies function return with table doc" ~: TestCase $ do+ let schemaContent =+ unlines+ [ "import { defineSchema, defineTable } from \"convex/server\";",+ "import { v } from \"convex/values\";",+ "",+ "export default defineSchema({",+ " users: defineTable({",+ " name: v.string(),",+ " email: v.string()",+ " })",+ "});"+ ]+ let apiFileContent =+ unlines+ [ "declare const fullApi: ApiFromModules<{",+ " \"myActions\": typeof myActions;",+ "}>;"+ ]+ let actionContent =+ unlines+ [ "export declare const getUser: import(\"convex/server\").RegisteredQuery<\"public\", {}, Promise<{ _id: import(\"convex/values\").GenericId<\"users\">; _creationTime: number; name: string; email: string; }>>;"+ ]+ let actionContents = [("myActions", actionContent)]++ result <- Parser.parseProjectFromContents schemaContent apiFileContent actionContents+ case result of+ Left err -> assertFailure ("Parser failed: " ++ show err)+ Right project ->+ let expected =+ [ Action.ConvexFunction+ { Action.funcName = "getUser",+ Action.funcPath = "myActions",+ Action.funcType = Action.Query,+ Action.funcArgs = [],+ Action.funcReturn = Schema.VReference "UsersDoc"+ }+ ]+ in Parser.ppFunctions project @?= expected++testUnifyWithNestedObject :: Test+testUnifyWithNestedObject = "unifies function return with nested object" ~: TestCase $ do+ let schemaContent =+ unlines+ [ "import { defineSchema, defineTable } from \"convex/server\";",+ "import { v } from \"convex/values\";",+ "",+ "export default defineSchema({",+ " users: defineTable({",+ " name: v.string(),",+ " profile: v.object({",+ " image: v.string()",+ " })",+ " })",+ "});"+ ]+ let apiFileContent =+ unlines+ [ "declare const fullApi: ApiFromModules<{",+ " \"myActions\": typeof myActions;",+ "}>;"+ ]+ let actionContent =+ unlines+ [ "export declare const getUser: import(\"convex/server\").RegisteredQuery<\"public\", {}, Promise<{ _id: import(\"convex/values\").GenericId<\"users\">; _creationTime: number; name: string; profile: { image: string; }; }>>;"+ ]+ let actionContents = [("myActions", actionContent)]++ result <- Parser.parseProjectFromContents schemaContent apiFileContent actionContents+ case result of+ Left err -> assertFailure ("Parser failed: " ++ show err)+ Right project ->+ let expected =+ [ Action.ConvexFunction+ { Action.funcName = "getUser",+ Action.funcPath = "myActions",+ Action.funcType = Action.Query,+ Action.funcArgs = [],+ Action.funcReturn = Schema.VReference "UsersDoc"+ }+ ]+ in Parser.ppFunctions project @?= expected++testUnifyWithArrayOfDocs :: Test+testUnifyWithArrayOfDocs = "unifies function returning an array of docs" ~: TestCase $ do+ let schemaContent =+ unlines+ [ "import { defineSchema, defineTable } from \"convex/server\";",+ "import { v } from \"convex/values\";",+ "",+ "export default defineSchema({",+ " users: defineTable({",+ " name: v.string(),",+ " email: v.string()",+ " })",+ "});"+ ]+ let apiFileContent =+ unlines+ [ "declare const fullApi: ApiFromModules<{",+ " \"myActions\": typeof myActions;",+ "}>;"+ ]+ let actionContent =+ unlines+ [ "export declare const listUsers: import(\"convex/server\").RegisteredQuery<\"public\", {}, Promise<{",+ " _id: import(\"convex/values\").GenericId<\"users\">;",+ " _creationTime: number;",+ " name: string;",+ " email: string;",+ "}[]>>;"+ ]+ let actionContents = [("myActions", actionContent)]++ result <- Parser.parseProjectFromContents schemaContent apiFileContent actionContents+ case result of+ Left err -> assertFailure ("Parser failed: " ++ show err)+ Right project ->+ let expected =+ [ Action.ConvexFunction+ { Action.funcName = "listUsers",+ Action.funcPath = "myActions",+ Action.funcType = Action.Query,+ Action.funcArgs = [],+ Action.funcReturn = Schema.VArray (Schema.VReference "UsersDoc")+ }+ ]+ in Parser.ppFunctions project @?= expected++testUnifyWithDeeplyNestedObject :: Test+testUnifyWithDeeplyNestedObject = "unifies function return with deeply nested object" ~: TestCase $ do+ let schemaContent =+ unlines+ [ "import { defineSchema, defineTable } from \"convex/server\";",+ "import { v } from \"convex/values\";",+ "",+ "export default defineSchema({",+ " users: defineTable({",+ " name: v.string(),",+ " profile: v.object({",+ " image: v.string(),",+ " details: v.object({",+ " bio: v.string()",+ " })",+ " })",+ " })",+ "});"+ ]+ let apiFileContent =+ unlines+ [ "declare const fullApi: ApiFromModules<{",+ " \"myActions\": typeof myActions;",+ "}>;"+ ]+ let actionContent =+ unlines+ [ "export declare const getUser: import(\"convex/server\").RegisteredQuery<\"public\", {}, Promise<{ _id: import(\"convex/values\").GenericId<\"users\">; _creationTime: number; name: string; profile: { image: string; details: { bio: string; } }; }>>;"+ ]+ let actionContents = [("myActions", actionContent)]++ result <- Parser.parseProjectFromContents schemaContent apiFileContent actionContents+ case result of+ Left err -> assertFailure ("Parser failed: " ++ show err)+ Right project ->+ let expected =+ [ Action.ConvexFunction+ { Action.funcName = "getUser",+ Action.funcPath = "myActions",+ Action.funcType = Action.Query,+ Action.funcArgs = [],+ Action.funcReturn = Schema.VReference "UsersDoc"+ }+ ]+ in Parser.ppFunctions project @?= expected++tests :: Test+tests =+ "Unification"+ ~: test+ [ testUnifyWithTableDoc,+ testUnifyWithNestedObject,+ testUnifyWithArrayOfDocs,+ testUnifyWithDeeplyNestedObject,+ testUnifyWithShuffledFields+ ]++testUnifyWithShuffledFields :: Test+testUnifyWithShuffledFields = "unifies function return with shuffled fields" ~: TestCase $ do+ let schemaContent =+ unlines+ [ "import { defineSchema, defineTable } from \"convex/server\";",+ "import { v } from \"convex/values\";",+ "",+ "export default defineSchema({",+ " users: defineTable({",+ " name: v.string(),",+ " email: v.string()",+ " })",+ "});"+ ]+ let apiFileContent =+ unlines+ [ "declare const fullApi: ApiFromModules<{",+ " \"myActions\": typeof myActions;",+ "}>;"+ ]+ let actionContent =+ unlines+ -- Note the shuffled fields: email, name instead of name, email+ [ "export declare const getUser: import(\"convex/server\").RegisteredQuery<\"public\", {}, Promise<{ _id: import(\"convex/values\").GenericId<\"users\">; _creationTime: number; email: string; name: string; }>>;"+ ]+ let actionContents = [("myActions", actionContent)]++ result <- Parser.parseProjectFromContents schemaContent apiFileContent actionContents+ case result of+ Left err -> assertFailure ("Parser failed: " ++ show err)+ Right project ->+ let expected =+ [ Action.ConvexFunction+ { Action.funcName = "getUser",+ Action.funcPath = "myActions",+ Action.funcType = Action.Query,+ Action.funcArgs = [],+ Action.funcReturn = Schema.VReference "UsersDoc"+ }+ ]+ in Parser.ppFunctions project @?= expected