From 00aff9b6db50287557be820a36b676c87dd88c06 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Tue, 5 Dec 2023 18:01:46 +0300 Subject: [PATCH 01/10] IP json schema tests, #18 --- package-lock.json | 345 ++++- package.json | 7 +- src/python-fastui/fastui/class_name.py | 1 + .../fastui/components/__init__.py | 1 + .../tests/react-fastui-json-schema.json | 1233 +++++++++++++++++ .../tests/test_json_schema_match.py | 57 + 6 files changed, 1641 insertions(+), 3 deletions(-) create mode 100644 src/python-fastui/tests/react-fastui-json-schema.json create mode 100644 src/python-fastui/tests/test_json_schema_match.py diff --git a/package-lock.json b/package-lock.json index 7b9a07f2..a5e581c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,7 @@ "requires": true, "packages": { "": { + "name": "FastUI", "workspaces": [ "src/*" ], @@ -21,7 +22,8 @@ "eslint-plugin-react-refresh": "^0.4.4", "eslint-plugin-simple-import-sort": "^10.0.0", "prettier": "^3.0.3", - "typescript": "^5.0.2" + "typescript": "^5.0.2", + "typescript-json-schema": "^0.62.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -237,6 +239,18 @@ "node": ">=6.9.0" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", @@ -825,6 +839,31 @@ "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1152,6 +1191,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=10" } @@ -1168,6 +1208,7 @@ "os": [ "darwin" ], + "peer": true, "engines": { "node": ">=10" } @@ -1184,6 +1225,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -1200,6 +1242,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -1216,6 +1259,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -1232,6 +1276,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -1248,6 +1293,7 @@ "os": [ "linux" ], + "peer": true, "engines": { "node": ">=10" } @@ -1264,6 +1310,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=10" } @@ -1280,6 +1327,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=10" } @@ -1296,6 +1344,7 @@ "os": [ "win32" ], + "peer": true, "engines": { "node": ">=10" } @@ -1320,6 +1369,30 @@ "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", "dev": true }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -1665,6 +1738,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", + "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1717,6 +1799,12 @@ "node": ">= 8" } }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2090,6 +2178,20 @@ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2143,6 +2245,12 @@ "node": ">=10" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2247,6 +2355,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2280,6 +2397,12 @@ "csstype": "^3.0.2" } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -2441,6 +2564,15 @@ "@esbuild/win32-x64": "0.19.5" } }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3116,6 +3248,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", @@ -3724,6 +3865,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-generator-function": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", @@ -4125,6 +4275,12 @@ "node": ">=10" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "node_modules/markdown-table": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", @@ -5190,6 +5346,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/path-equal": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/path-equal/-/path-equal-1.2.5.tgz", + "integrity": "sha512-i73IctDr3F2W+bsOWDyyVm/lqsXO47aY9nsFZUjTT/aljSbkxHxxCoyZ9UUrM8jK0JVod+An+rl48RCsvWM+9g==", + "dev": true + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5641,6 +5803,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -5784,6 +5955,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/sass": { "version": "1.69.5", "resolved": "https://registry.npmjs.org/sass/-/sass-1.69.5.tgz", @@ -5921,6 +6101,20 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz", @@ -6111,6 +6305,49 @@ "typescript": ">=4.2.0" } }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -6231,6 +6468,44 @@ "node": ">=14.17" } }, + "node_modules/typescript-json-schema": { + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.62.0.tgz", + "integrity": "sha512-qRO6pCgyjKJ230QYdOxDRpdQrBeeino4v5p2rYmSD72Jf4rD3O+cJcROv46sQukm46CLWoeusqvBgKpynEv25g==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "@types/node": "^16.9.2", + "glob": "^7.1.7", + "path-equal": "^1.2.5", + "safe-stable-stringify": "^2.2.0", + "ts-node": "^10.9.1", + "typescript": "~5.1.0", + "yargs": "^17.1.1" + }, + "bin": { + "typescript-json-schema": "bin/typescript-json-schema" + } + }, + "node_modules/typescript-json-schema/node_modules/@types/node": { + "version": "16.18.67", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.67.tgz", + "integrity": "sha512-gUa0tDO9oxyAYO9V9tqxDJguVMDpqUwH5I5Q9ASYBCso+8CUdJlKPKDYS1YSS9kyZWIduDafZvucGM0zGNKFjg==", + "dev": true + }, + "node_modules/typescript-json-schema/node_modules/typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -6369,6 +6644,12 @@ } } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, "node_modules/vfile": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", @@ -6551,6 +6832,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -6565,6 +6863,15 @@ "node": ">=0.4" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", @@ -6579,6 +6886,42 @@ "node": ">= 6" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index a6216e35..1547673e 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,5 @@ { + "name": "FastUI", "private": true, "type": "module", "workspaces": [ @@ -11,7 +12,8 @@ "lint": "eslint src --ext .ts,.tsx --report-unused-disable-directives --max-warnings 0", "lint-fix": "npm run lint -- --fix", "prettier": "prettier --write", - "format": "npm run prettier -- . && npm run lint-fix" + "format": "npm run prettier -- . && npm run lint-fix", + "generate-json-schema": "typescript-json-schema src/npm-fastui/tsconfig.json FastProps > src/python-fastui/tests/react-fastui-json-schema.json" }, "prettier": { "singleQuote": true, @@ -35,6 +37,7 @@ "eslint-plugin-react-refresh": "^0.4.4", "eslint-plugin-simple-import-sort": "^10.0.0", "prettier": "^3.0.3", - "typescript": "^5.0.2" + "typescript": "^5.0.2", + "typescript-json-schema": "^0.62.0" } } diff --git a/src/python-fastui/fastui/class_name.py b/src/python-fastui/fastui/class_name.py index 5bd31bc5..b8c8dd16 100644 --- a/src/python-fastui/fastui/class_name.py +++ b/src/python-fastui/fastui/class_name.py @@ -4,4 +4,5 @@ from pydantic import Field from typing_extensions import Annotated +# should be `str | List[ClassName] | Dict[str, bool | None] | None`, but pydantic doesn't like that ClassName = Annotated[Union[str, List[str], Dict[str, Union[bool, None]], None], Field(serialization_alias='className')] diff --git a/src/python-fastui/fastui/components/__init__.py b/src/python-fastui/fastui/components/__init__.py index 5cdcb223..1311e907 100644 --- a/src/python-fastui/fastui/components/__init__.py +++ b/src/python-fastui/fastui/components/__init__.py @@ -57,6 +57,7 @@ class Text(pydantic.BaseModel, extra='forbid'): class Paragraph(pydantic.BaseModel, extra='forbid'): text: str + class_name: _class_name.ClassName = None type: _t.Literal['Paragraph'] = 'Paragraph' diff --git a/src/python-fastui/tests/react-fastui-json-schema.json b/src/python-fastui/tests/react-fastui-json-schema.json new file mode 100644 index 00000000..05b70161 --- /dev/null +++ b/src/python-fastui/tests/react-fastui-json-schema.json @@ -0,0 +1,1233 @@ +{ + "$ref": "#/definitions/FastProps", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "BackEvent": { + "properties": { + "type": { + "const": "back", + "type": "string" + } + }, + "type": "object" + }, + "ButtonProps": { + "properties": { + "className": { + "$ref": "#/definitions/ClassName" + }, + "htmlType": { + "enum": ["button", "reset", "submit"], + "type": "string" + }, + "onClick": { + "anyOf": [ + { + "$ref": "#/definitions/PageEvent" + }, + { + "$ref": "#/definitions/GoToEvent" + }, + { + "$ref": "#/definitions/BackEvent" + } + ] + }, + "text": { + "type": "string" + }, + "type": { + "const": "Button", + "type": "string" + } + }, + "type": "object" + }, + "ClassName": { + "anyOf": [ + { + "items": { + "$ref": "#/definitions/ClassName" + }, + "type": "array" + }, + { + "$ref": "#/definitions/Record" + }, + { + "type": "string" + } + ] + }, + "CodeProps": { + "properties": { + "className": { + "$ref": "#/definitions/ClassName" + }, + "codeStyle": { + "type": "string" + }, + "language": { + "type": "string" + }, + "text": { + "type": "string" + }, + "type": { + "const": "Code", + "type": "string" + } + }, + "type": "object" + }, + "ContextType": { + "type": "object" + }, + "DetailsProps": { + "properties": { + "className": { + "$ref": "#/definitions/ClassName" + }, + "data": { + "$ref": "#/definitions/ModelData" + }, + "fields": { + "items": { + "$ref": "#/definitions/DisplayLookupProps" + }, + "type": "array" + }, + "type": { + "const": "Details", + "type": "string" + } + }, + "type": "object" + }, + "DisplayArrayProps": { + "properties": { + "mode": { + "enum": ["as_title", "auto", "date", "datetime", "duration", "inline_code", "json", "markdown", "plain"], + "type": "string" + }, + "type": { + "const": "DisplayArray", + "type": "string" + }, + "value": { + "items": { + "$ref": "#/definitions/JsonData" + }, + "type": "array" + } + }, + "type": "object" + }, + "DisplayLookupProps": { + "properties": { + "field": { + "type": "string" + }, + "mode": { + "enum": ["as_title", "auto", "date", "datetime", "duration", "inline_code", "json", "markdown", "plain"], + "type": "string" + }, + "onClick": { + "anyOf": [ + { + "$ref": "#/definitions/PageEvent" + }, + { + "$ref": "#/definitions/GoToEvent" + }, + { + "$ref": "#/definitions/BackEvent" + } + ] + }, + "tableWidthPercent": { + "type": "number" + }, + "title": { + "type": "string" + } + }, + "type": "object" + }, + "DisplayMode": { + "enum": ["auto", "plain", "datetime", "date", "duration", "as_title", "markdown", "json", "inline_code"], + "type": "string" + }, + "DisplayObjectProps": { + "properties": { + "mode": { + "enum": ["as_title", "auto", "date", "datetime", "duration", "inline_code", "json", "markdown", "plain"], + "type": "string" + }, + "type": { + "const": "DisplayObject", + "type": "string" + }, + "value": { + "additionalProperties": { + "$ref": "#/definitions/JsonData" + }, + "type": "object" + } + }, + "type": "object" + }, + "DisplayPrimitiveProps": { + "properties": { + "mode": { + "$ref": "#/definitions/DisplayMode" + }, + "type": { + "const": "DisplayPrimitive", + "type": "string" + }, + "value": { + "$ref": "#/definitions/JSONPrimitive" + } + }, + "type": "object" + }, + "DisplayProps": { + "properties": { + "mode": { + "enum": ["as_title", "auto", "date", "datetime", "duration", "inline_code", "json", "markdown", "plain"], + "type": "string" + }, + "onClick": { + "anyOf": [ + { + "$ref": "#/definitions/PageEvent" + }, + { + "$ref": "#/definitions/GoToEvent" + }, + { + "$ref": "#/definitions/BackEvent" + } + ] + }, + "title": { + "type": "string" + }, + "type": { + "const": "Display", + "type": "string" + }, + "value": { + "anyOf": [ + { + "items": { + "$ref": "#/definitions/JsonData" + }, + "type": "array" + }, + { + "additionalProperties": { + "$ref": "#/definitions/JsonData" + }, + "type": "object" + }, + { + "type": ["null", "string", "number", "boolean"] + } + ] + } + }, + "type": "object" + }, + "DivProps": { + "properties": { + "className": { + "$ref": "#/definitions/ClassName" + }, + "components": { + "items": { + "$ref": "#/definitions/FastProps" + }, + "type": "array" + }, + "type": { + "const": "Div", + "type": "string" + } + }, + "type": "object" + }, + "FastProps": { + "anyOf": [ + { + "$ref": "#/definitions/TextProps" + }, + { + "$ref": "#/definitions/ParagraphProps" + }, + { + "$ref": "#/definitions/PageTitleProps" + }, + { + "$ref": "#/definitions/DivProps" + }, + { + "$ref": "#/definitions/PageProps" + }, + { + "$ref": "#/definitions/HeadingProps" + }, + { + "$ref": "#/definitions/MarkdownProps" + }, + { + "$ref": "#/definitions/CodeProps" + }, + { + "$ref": "#/definitions/FormProps" + }, + { + "$ref": "#/definitions/ModelFormProps" + }, + { + "$ref": "#/definitions/FormFieldInputProps" + }, + { + "$ref": "#/definitions/FormFieldBooleanProps" + }, + { + "$ref": "#/definitions/FormFieldFileProps" + }, + { + "$ref": "#/definitions/FormFieldSelectProps" + }, + { + "$ref": "#/definitions/FormFieldSelectSearchProps" + }, + { + "$ref": "#/definitions/ButtonProps" + }, + { + "$ref": "#/definitions/ModalProps" + }, + { + "$ref": "#/definitions/TableProps" + }, + { + "$ref": "#/definitions/PaginationProps" + }, + { + "$ref": "#/definitions/DetailsProps" + }, + { + "$ref": "#/definitions/LinkProps" + }, + { + "$ref": "#/definitions/LinkListProps" + }, + { + "$ref": "#/definitions/NavbarProps" + }, + { + "$ref": "#/definitions/DisplayProps" + }, + { + "$ref": "#/definitions/DisplayArrayProps" + }, + { + "$ref": "#/definitions/DisplayObjectProps" + }, + { + "$ref": "#/definitions/DisplayPrimitiveProps" + }, + { + "$ref": "#/definitions/JsonProps" + }, + { + "$ref": "#/definitions/ServerLoadProps" + }, + { + "$ref": "#/definitions/ImageProps" + }, + { + "$ref": "#/definitions/IframeProps" + } + ] + }, + "FormFieldBooleanProps": { + "properties": { + "className": { + "$ref": "#/definitions/ClassName" + }, + "description": { + "type": "string" + }, + "displayMode": { + "enum": ["default", "inline"], + "type": "string" + }, + "error": { + "type": "string" + }, + "initial": { + "type": "boolean" + }, + "locked": { + "type": "boolean" + }, + "mode": { + "enum": ["checkbox", "switch"], + "type": "string" + }, + "name": { + "type": "string" + }, + "onChange": { + "type": "object" + }, + "required": { + "type": "boolean" + }, + "title": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "const": "FormFieldBoolean", + "type": "string" + } + }, + "type": "object" + }, + "FormFieldFileProps": { + "properties": { + "accept": { + "type": "string" + }, + "className": { + "$ref": "#/definitions/ClassName" + }, + "description": { + "type": "string" + }, + "displayMode": { + "enum": ["default", "inline"], + "type": "string" + }, + "error": { + "type": "string" + }, + "locked": { + "type": "boolean" + }, + "multiple": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "onChange": { + "type": "object" + }, + "required": { + "type": "boolean" + }, + "title": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "const": "FormFieldFile", + "type": "string" + } + }, + "type": "object" + }, + "FormFieldInputProps": { + "properties": { + "className": { + "$ref": "#/definitions/ClassName" + }, + "description": { + "type": "string" + }, + "displayMode": { + "enum": ["default", "inline"], + "type": "string" + }, + "error": { + "type": "string" + }, + "htmlType": { + "enum": ["date", "datetime-local", "email", "number", "password", "text", "time", "url"], + "type": "string" + }, + "initial": { + "type": ["string", "number"] + }, + "locked": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "onChange": { + "type": "object" + }, + "placeholder": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "title": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "const": "FormFieldInput", + "type": "string" + } + }, + "type": "object" + }, + "FormFieldProps": { + "anyOf": [ + { + "$ref": "#/definitions/FormFieldInputProps" + }, + { + "$ref": "#/definitions/FormFieldBooleanProps" + }, + { + "$ref": "#/definitions/FormFieldFileProps" + }, + { + "$ref": "#/definitions/FormFieldSelectProps" + }, + { + "$ref": "#/definitions/FormFieldSelectSearchProps" + } + ] + }, + "FormFieldSelectProps": { + "properties": { + "className": { + "$ref": "#/definitions/ClassName" + }, + "description": { + "type": "string" + }, + "displayMode": { + "enum": ["default", "inline"], + "type": "string" + }, + "error": { + "type": "string" + }, + "initial": { + "type": "string" + }, + "locked": { + "type": "boolean" + }, + "multiple": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "onChange": { + "type": "object" + }, + "options": { + "$ref": "#/definitions/SelectOptions" + }, + "placeholder": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "title": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "const": "FormFieldSelect", + "type": "string" + }, + "vanilla": { + "type": "boolean" + } + }, + "type": "object" + }, + "FormFieldSelectSearchProps": { + "properties": { + "className": { + "$ref": "#/definitions/ClassName" + }, + "debounce": { + "type": "number" + }, + "description": { + "type": "string" + }, + "displayMode": { + "enum": ["default", "inline"], + "type": "string" + }, + "error": { + "type": "string" + }, + "initial": { + "$ref": "#/definitions/SelectOption" + }, + "locked": { + "type": "boolean" + }, + "multiple": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "onChange": { + "type": "object" + }, + "placeholder": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "searchUrl": { + "type": "string" + }, + "title": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "const": "FormFieldSelectSearch", + "type": "string" + } + }, + "type": "object" + }, + "FormProps": { + "properties": { + "className": { + "$ref": "#/definitions/ClassName" + }, + "displayMode": { + "enum": ["default", "inline"], + "type": "string" + }, + "footer": { + "anyOf": [ + { + "items": { + "$ref": "#/definitions/FastProps" + }, + "type": "array" + }, + { + "type": "boolean" + } + ] + }, + "formFields": { + "items": { + "$ref": "#/definitions/FormFieldProps" + }, + "type": "array" + }, + "initial": { + "$ref": "#/definitions/Record" + }, + "method": { + "enum": ["GET", "GOTO", "POST"], + "type": "string" + }, + "submitOnChange": { + "type": "boolean" + }, + "submitUrl": { + "type": "string" + }, + "type": { + "const": "Form", + "type": "string" + } + }, + "type": "object" + }, + "GoToEvent": { + "properties": { + "query": { + "$ref": "#/definitions/Record" + }, + "type": { + "const": "go-to", + "type": "string" + }, + "url": { + "type": "string" + } + }, + "type": "object" + }, + "HeadingProps": { + "properties": { + "className": { + "$ref": "#/definitions/ClassName" + }, + "htmlId": { + "type": "string" + }, + "level": { + "enum": [1, 2, 3, 4, 5, 6], + "type": "number" + }, + "text": { + "type": "string" + }, + "type": { + "const": "Heading", + "type": "string" + } + }, + "type": "object" + }, + "IframeProps": { + "properties": { + "className": { + "$ref": "#/definitions/ClassName" + }, + "height": { + "type": ["string", "number"] + }, + "src": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "const": "Iframe", + "type": "string" + }, + "width": { + "type": ["string", "number"] + } + }, + "type": "object" + }, + "ImageProps": { + "properties": { + "alt": { + "type": "string" + }, + "className": { + "$ref": "#/definitions/ClassName" + }, + "height": { + "type": ["string", "number"] + }, + "loading": { + "enum": ["eager", "lazy"], + "type": "string" + }, + "onClick": { + "anyOf": [ + { + "$ref": "#/definitions/PageEvent" + }, + { + "$ref": "#/definitions/GoToEvent" + }, + { + "$ref": "#/definitions/BackEvent" + } + ] + }, + "referrerPolicy": { + "enum": [ + "no-referrer", + "no-referrer-when-downgrade", + "origin", + "origin-when-cross-origin", + "same-origin", + "strict-origin", + "strict-origin-when-cross-origin", + "unsafe-url" + ], + "type": "string" + }, + "src": { + "type": "string" + }, + "type": { + "const": "Image", + "type": "string" + }, + "width": { + "type": ["string", "number"] + } + }, + "type": "object" + }, + "JSONPrimitive": { + "type": ["null", "string", "number", "boolean"] + }, + "JsonData": { + "anyOf": [ + { + "items": { + "$ref": "#/definitions/JsonData" + }, + "type": "array" + }, + { + "additionalProperties": { + "$ref": "#/definitions/JsonData" + }, + "type": "object" + }, + { + "type": ["null", "string", "number", "boolean"] + } + ] + }, + "JsonProps": { + "properties": { + "className": { + "$ref": "#/definitions/ClassName" + }, + "type": { + "const": "JSON", + "type": "string" + }, + "value": { + "$ref": "#/definitions/JsonData" + } + }, + "type": "object" + }, + "LinkListProps": { + "properties": { + "className": { + "$ref": "#/definitions/ClassName" + }, + "links": { + "items": { + "$ref": "#/definitions/LinkProps" + }, + "type": "array" + }, + "mode": { + "enum": ["pagination", "tabs", "vertical"], + "type": "string" + }, + "type": { + "const": "LinkList", + "type": "string" + } + }, + "type": "object" + }, + "LinkProps": { + "properties": { + "active": { + "type": ["string", "boolean"] + }, + "className": { + "$ref": "#/definitions/ClassName" + }, + "components": { + "items": { + "$ref": "#/definitions/FastProps" + }, + "type": "array" + }, + "locked": { + "type": "boolean" + }, + "mode": { + "enum": ["navbar", "pagination", "tabs", "vertical"], + "type": "string" + }, + "onClick": { + "anyOf": [ + { + "$ref": "#/definitions/PageEvent" + }, + { + "$ref": "#/definitions/GoToEvent" + }, + { + "$ref": "#/definitions/BackEvent" + } + ] + }, + "type": { + "const": "Link", + "type": "string" + } + }, + "type": "object" + }, + "MarkdownProps": { + "properties": { + "className": { + "$ref": "#/definitions/ClassName" + }, + "codeStyle": { + "type": "string" + }, + "text": { + "type": "string" + }, + "type": { + "const": "Markdown", + "type": "string" + } + }, + "type": "object" + }, + "ModalProps": { + "properties": { + "body": { + "items": { + "$ref": "#/definitions/FastProps" + }, + "type": "array" + }, + "className": { + "$ref": "#/definitions/ClassName" + }, + "footer": { + "items": { + "$ref": "#/definitions/FastProps" + }, + "type": "array" + }, + "openContext": { + "$ref": "#/definitions/ContextType" + }, + "openTrigger": { + "$ref": "#/definitions/PageEvent" + }, + "title": { + "type": "string" + }, + "type": { + "const": "Modal", + "type": "string" + } + }, + "type": "object" + }, + "ModelData": { + "type": "object" + }, + "ModelFormProps": { + "properties": { + "className": { + "$ref": "#/definitions/ClassName" + }, + "displayMode": { + "enum": ["default", "inline"], + "type": "string" + }, + "footer": { + "anyOf": [ + { + "items": { + "$ref": "#/definitions/FastProps" + }, + "type": "array" + }, + { + "type": "boolean" + } + ] + }, + "formFields": { + "items": { + "$ref": "#/definitions/FormFieldProps" + }, + "type": "array" + }, + "initial": { + "$ref": "#/definitions/Record" + }, + "method": { + "enum": ["GET", "GOTO", "POST"], + "type": "string" + }, + "submitOnChange": { + "type": "boolean" + }, + "submitUrl": { + "type": "string" + }, + "type": { + "const": "ModelForm", + "type": "string" + } + }, + "type": "object" + }, + "NavbarProps": { + "properties": { + "className": { + "$ref": "#/definitions/ClassName" + }, + "links": { + "items": { + "$ref": "#/definitions/LinkProps" + }, + "type": "array" + }, + "title": { + "type": "string" + }, + "titleEvent": { + "anyOf": [ + { + "$ref": "#/definitions/PageEvent" + }, + { + "$ref": "#/definitions/GoToEvent" + }, + { + "$ref": "#/definitions/BackEvent" + } + ] + }, + "type": { + "const": "Navbar", + "type": "string" + } + }, + "type": "object" + }, + "PageEvent": { + "properties": { + "clear": { + "type": "boolean" + }, + "context": { + "$ref": "#/definitions/ContextType" + }, + "name": { + "type": "string" + }, + "pushPath": { + "type": "string" + }, + "type": { + "const": "page", + "type": "string" + } + }, + "type": "object" + }, + "PageProps": { + "properties": { + "className": { + "$ref": "#/definitions/ClassName" + }, + "components": { + "items": { + "$ref": "#/definitions/FastProps" + }, + "type": "array" + }, + "type": { + "const": "Page", + "type": "string" + } + }, + "type": "object" + }, + "PageTitleProps": { + "properties": { + "text": { + "type": "string" + }, + "type": { + "const": "PageTitle", + "type": "string" + } + }, + "type": "object" + }, + "PaginationProps": { + "properties": { + "className": { + "$ref": "#/definitions/ClassName" + }, + "page": { + "type": "number" + }, + "pageCount": { + "type": "number" + }, + "pageSize": { + "type": "number" + }, + "total": { + "type": "number" + }, + "type": { + "const": "Pagination", + "type": "string" + } + }, + "type": "object" + }, + "ParagraphProps": { + "properties": { + "className": { + "$ref": "#/definitions/ClassName" + }, + "text": { + "type": "string" + }, + "type": { + "const": "Paragraph", + "type": "string" + } + }, + "type": "object" + }, + "Record": { + "type": "object" + }, + "Record": { + "type": "object" + }, + "Record": { + "type": "object" + }, + "SelectGroup": { + "properties": { + "label": { + "type": "string" + }, + "options": { + "items": { + "$ref": "#/definitions/SelectOption" + }, + "type": "array" + } + }, + "type": "object" + }, + "SelectOption": { + "properties": { + "label": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "type": "object" + }, + "SelectOptions": { + "anyOf": [ + { + "items": { + "$ref": "#/definitions/SelectOption" + }, + "type": "array" + }, + { + "items": { + "$ref": "#/definitions/SelectGroup" + }, + "type": "array" + } + ] + }, + "ServerLoadProps": { + "properties": { + "components": { + "items": { + "$ref": "#/definitions/FastProps" + }, + "type": "array" + }, + "loadTrigger": { + "$ref": "#/definitions/PageEvent" + }, + "path": { + "type": "string" + }, + "sse": { + "type": "boolean" + }, + "type": { + "const": "ServerLoad", + "type": "string" + } + }, + "type": "object" + }, + "TableProps": { + "properties": { + "className": { + "$ref": "#/definitions/ClassName" + }, + "columns": { + "items": { + "$ref": "#/definitions/DisplayLookupProps" + }, + "type": "array" + }, + "data": { + "items": { + "$ref": "#/definitions/ModelData" + }, + "type": "array" + }, + "noDataMessage": { + "type": "string" + }, + "type": { + "const": "Table", + "type": "string" + } + }, + "type": "object" + }, + "TextProps": { + "properties": { + "text": { + "type": "string" + }, + "type": { + "const": "Text", + "type": "string" + } + }, + "type": "object" + } + } +} diff --git a/src/python-fastui/tests/test_json_schema_match.py b/src/python-fastui/tests/test_json_schema_match.py new file mode 100644 index 00000000..646ca7cc --- /dev/null +++ b/src/python-fastui/tests/test_json_schema_match.py @@ -0,0 +1,57 @@ +import json +import typing +from pathlib import Path + +import pytest +from fastui import AnyComponent +from pydantic import TypeAdapter + +THIS_DIR = Path(__file__).parent +with (THIS_DIR / 'react-fastui-json-schema.json').open('rb') as npm_file: + REACT_SCHEMA_DEFS = json.load(npm_file)['definitions'] + + +def python_model_iter(): + ta = TypeAdapter(AnyComponent) + json_schema = ta.json_schema(by_alias=True, mode='serialization') + defs = json_schema['$defs'] + for d in json_schema['oneOf']: + key = d['$ref'].split('/')[-1] + yield pytest.param(defs[key], id=key) + + +@pytest.mark.parametrize('model_schema', python_model_iter()) +def test_components_match(model_schema: typing.Dict[str, typing.Any]): + title = model_schema.get('title') + if title is None: + all_of = model_schema['allOf'] + assert len(all_of) == 1 + ref = all_of[0]['$ref'] + title = ref.split('/')[-1] + model_schema = model_schema['$defs'][title] + + react_title = f'{title}Props' + + try: + react_schema = REACT_SCHEMA_DEFS[react_title] + except KeyError as e: + pytest.fail(f'No react model found with name {e}') + + model_properties = model_schema['properties'] + for value in model_properties.values(): + value.pop('title', None) + value.pop('default', None) + + react_properties = react_schema['properties'] + # typescript-json-schema adds type to `const` properties while pydantic does not, + # pydantic matches the example from JSON Schema's docs + # https://json-schema.org/understanding-json-schema/reference/const + react_properties['type'].pop('type') + + if 'className' in model_properties and 'className' in react_properties: + # class name doesn't match due to recursive type + model_properties.pop('className') + react_properties.pop('className') + + # debug(model_properties, react_properties) + assert model_properties == react_properties From 12e83d8bf1c3d0f7c70bca07e1119cc386b47584 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Wed, 6 Dec 2023 17:54:20 +0300 Subject: [PATCH 02/10] fixing more cases --- .../fastui/components/__init__.py | 2 +- .../tests/test_json_schema_match.py | 50 +++++++++++++++++-- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/python-fastui/fastui/components/__init__.py b/src/python-fastui/fastui/components/__init__.py index 1311e907..0c2c98c4 100644 --- a/src/python-fastui/fastui/components/__init__.py +++ b/src/python-fastui/fastui/components/__init__.py @@ -117,7 +117,7 @@ class Code(pydantic.BaseModel, extra='forbid'): class Button(pydantic.BaseModel, extra='forbid'): text: str on_click: _t.Union[events.AnyEvent, None] = pydantic.Field(default=None, serialization_alias='onClick') - html_type: _t.Union[_t.Literal['button', 'submit', 'reset'], None] = pydantic.Field( + html_type: _t.Union[_t.Literal['button', 'reset', 'submit'], None] = pydantic.Field( default=None, serialization_alias='htmlType' ) class_name: _class_name.ClassName = None diff --git a/src/python-fastui/tests/test_json_schema_match.py b/src/python-fastui/tests/test_json_schema_match.py index 646ca7cc..3fdf4681 100644 --- a/src/python-fastui/tests/test_json_schema_match.py +++ b/src/python-fastui/tests/test_json_schema_match.py @@ -10,14 +10,52 @@ with (THIS_DIR / 'react-fastui-json-schema.json').open('rb') as npm_file: REACT_SCHEMA_DEFS = json.load(npm_file)['definitions'] +ta = TypeAdapter(AnyComponent) +json_schema = ta.json_schema(by_alias=True, mode='serialization', ref_template='#/definitions/{model}') +components_union = json_schema['oneOf'] +components_union_set = set(d['$ref'] for d in components_union) + def python_model_iter(): - ta = TypeAdapter(AnyComponent) - json_schema = ta.json_schema(by_alias=True, mode='serialization') - defs = json_schema['$defs'] - for d in json_schema['oneOf']: + python_defs = json_schema['$defs'] + for d in components_union: key = d['$ref'].split('/')[-1] - yield pytest.param(defs[key], id=key) + yield pytest.param(python_defs[key], id=key) + + +def fix_pydantic_schema(s: typing.Any): + """ + * convert pydantic's `anyOf` all components to typescripts `'$ref': '#/definitions/FastProps'` + * switch "type": "integer" to "type": "number" + * switch `{"anyOf": [{"type": "XXX"}, {"type": "null"}]}` to `{"type": "XXX"}` + * switch `{"oneOf": ..., "discriminator": ...}` to `{"anyOf": ...}` + """ + if isinstance(s, dict): + for k, v in s.items(): + if k == 'type' and v == 'integer': + s[k] = 'number' + if isinstance(v, dict): + if any_of := v.get('anyOf'): + refs = [d.get('$ref') for d in any_of] + if set(refs) == components_union_set: + v.pop('anyOf') + v['$ref'] = '#/definitions/FastProps' + continue + + null_type = {'type': 'null'} + if null_type in any_of and len(any_of) == 2: + other = next(d for d in any_of if d != null_type) + v.clear() + v.update(other) + + if 'discriminator' in v and (one_of := v.get('oneOf')): + v.clear() + v['anyOf'] = one_of + + fix_pydantic_schema(v) + elif isinstance(s, list): + for item in s: + fix_pydantic_schema(item) @pytest.mark.parametrize('model_schema', python_model_iter()) @@ -42,6 +80,8 @@ def test_components_match(model_schema: typing.Dict[str, typing.Any]): value.pop('title', None) value.pop('default', None) + fix_pydantic_schema(model_properties) + react_properties = react_schema['properties'] # typescript-json-schema adds type to `const` properties while pydantic does not, # pydantic matches the example from JSON Schema's docs From f25a834004eed462c8d83844fc47944f915e5102 Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Thu, 7 Dec 2023 11:51:42 +0300 Subject: [PATCH 03/10] make AnyComponent a TypeAliasType --- .../fastui/components/__init__.py | 57 ++++++++++--------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/src/python-fastui/fastui/components/__init__.py b/src/python-fastui/fastui/components/__init__.py index 0c2c98c4..c9f2254e 100644 --- a/src/python-fastui/fastui/components/__init__.py +++ b/src/python-fastui/fastui/components/__init__.py @@ -203,31 +203,34 @@ class Iframe(pydantic.BaseModel, extra='forbid'): type: _t.Literal['Iframe'] = 'Iframe' -AnyComponent = _te.Annotated[ - _t.Union[ - Text, - Paragraph, - PageTitle, - Div, - Page, - Heading, - Markdown, - Code, - Button, - Link, - LinkList, - Navbar, - Modal, - ServerLoad, - Table, - Pagination, - Display, - Details, - Form, - ModelForm, - Image, - Iframe, - FormField, +AnyComponent = _te.TypeAliasType( + 'AnyComponent', + _te.Annotated[ + _t.Union[ + Text, + Paragraph, + PageTitle, + Div, + Page, + Heading, + Markdown, + Code, + Button, + Link, + LinkList, + Navbar, + Modal, + ServerLoad, + Table, + Pagination, + Display, + Details, + Form, + ModelForm, + Image, + Iframe, + FormField, + ], + pydantic.Field(discriminator='type'), ], - pydantic.Field(discriminator='type'), -] +) From 19c5660274496437fde8fac69549819ef54eb25f Mon Sep 17 00:00:00 2001 From: Samuel Colvin Date: Mon, 11 Dec 2023 10:57:19 +0000 Subject: [PATCH 04/10] tests passing! --- src/npm-fastui/src/components/FormField.tsx | 12 +- src/npm-fastui/src/components/Iframe.tsx | 7 + src/npm-fastui/src/components/display.tsx | 1 + src/npm-fastui/src/components/form.tsx | 1 + src/npm-fastui/src/components/heading.tsx | 4 + src/npm-fastui/src/components/image.tsx | 2 + src/npm-fastui/src/components/pagination.tsx | 4 + .../fastui/components/__init__.py | 69 +++++----- .../fastui/components/display.py | 8 +- src/python-fastui/fastui/components/forms.py | 13 +- src/python-fastui/fastui/components/tables.py | 2 +- src/python-fastui/fastui/events.py | 6 +- src/python-fastui/fastui/forms.py | 5 +- .../tests/react-fastui-json-schema.json | 123 ++++++++++-------- .../tests/test_json_schema_match.py | 114 +++++++++++----- 15 files changed, 232 insertions(+), 139 deletions(-) diff --git a/src/npm-fastui/src/components/FormField.tsx b/src/npm-fastui/src/components/FormField.tsx index aa8a4edd..0fd0e44e 100644 --- a/src/npm-fastui/src/components/FormField.tsx +++ b/src/npm-fastui/src/components/FormField.tsx @@ -5,16 +5,18 @@ import Select, { StylesConfig } from 'react-select' import { ClassName, useClassName } from '../hooks/className' import { debounce, useRequest } from '../tools' +type PrivateOnChange = () => void + interface BaseFormFieldProps { name: string - title: string[] + title: string | string[] required: boolean locked: boolean error?: string description?: string displayMode?: 'default' | 'inline' className?: ClassName - onChange?: () => void + onChange?: PrivateOnChange } export type FormFieldProps = @@ -239,6 +241,7 @@ function findDefault(options: SelectOptions, value?: string): SelectOption | und interface FormFieldSelectSearchProps extends BaseFormFieldProps { type: 'FormFieldSelectSearch' searchUrl: string + /** @TJS-type integer */ debounce?: number initial?: SelectOption multiple?: boolean @@ -302,7 +305,10 @@ export const FormFieldSelectSearchComp: FC = (props) } const Label: FC = (props) => { - const { title } = props + let { title } = props + if (!Array.isArray(title)) { + title = [title] + } return (