From ea6f3865c8e381b3160e1526435c4522f0dc6aa4 Mon Sep 17 00:00:00 2001 From: Lubos Date: Fri, 13 Feb 2026 21:15:47 +0000 Subject: [PATCH] refactor: valibot plugin nullish --- pyproject.toml | 2 +- uv.lock | 169 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ .changeset/brown-ideas-matter.md | 5 +++++ .changeset/clean-singers-stand.md | 9 +++++++++ .changeset/stale-falcons-raise.md | 5 +++++ docs/openapi-ts/migrating.md | 6 ++++++ packages/shared/src/index.ts | 16 +++++++++------- packages/shared/src/ir/schema-processor.ts | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ packages/shared/src/ir/schema-walker.ts | 283 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ packages/shared/src/parser/hooks.ts | 2 +- packages/shared/src/plugins/schema-processor.ts | 70 ---------------------------------------------------------------------- packages/openapi-ts/src/plugins/zod/constants.ts | 1 + packages/openapi-python/src/plugins/pydantic/shared/export.ts | 37 ++++++++++++++++++++++++------------- packages/openapi-python/src/plugins/pydantic/shared/processor.ts | 16 ++++++++++++++++ packages/openapi-python/src/plugins/pydantic/shared/types.ts | 3 ++- packages/openapi-python/src/plugins/pydantic/v2/plugin.ts | 84 ++++++++++++++++++++++++++++++++---------------------------------------------------- packages/openapi-python/src/plugins/pydantic/v2/processor.ts | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ packages/openapi-ts/src/plugins/valibot/resolvers/types.ts | 19 +++++++++++++------ packages/openapi-ts/src/plugins/valibot/shared/types.ts | 6 ++---- packages/openapi-ts/src/plugins/valibot/v1/plugin.ts | 151 +------------------------------------------------------------------------------------------------------------------------------------------------------ packages/openapi-ts/src/plugins/valibot/v1/processor.ts | 24 ++++++++++++++++++------ packages/openapi-ts/src/plugins/valibot/v1/walker.ts | 358 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/validators/valibot.gen.ts | 12 ++++++------ packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-circular-ref-2/valibot.gen.ts | 2 +- packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-metadata/valibot.gen.ts | 12 ++++++------ packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-types/valibot.gen.ts | 12 ++++++------ packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators/valibot.gen.ts | 12 ++++++------ packages/openapi-ts/src/plugins/valibot/v1/toAst/array.ts | 34 +++++++++++++++++++--------------- packages/openapi-ts/src/plugins/valibot/v1/toAst/boolean.ts | 2 +- packages/openapi-ts/src/plugins/valibot/v1/toAst/enum.ts | 20 ++------------------ packages/openapi-ts/src/plugins/valibot/v1/toAst/index.ts | 130 ---------------------------------------------------------------------------------------------------------------------------------- packages/openapi-ts/src/plugins/valibot/v1/toAst/never.ts | 2 +- packages/openapi-ts/src/plugins/valibot/v1/toAst/null.ts | 2 +- packages/openapi-ts/src/plugins/valibot/v1/toAst/number.ts | 2 +- packages/openapi-ts/src/plugins/valibot/v1/toAst/object.ts | 80 ++++++++++++++++++++++++++++++++++++++++++++------------------------------------ packages/openapi-ts/src/plugins/valibot/v1/toAst/string.ts | 2 +- packages/openapi-ts/src/plugins/valibot/v1/toAst/tuple.ts | 33 +++++++++++++++++++-------------- packages/openapi-ts/src/plugins/valibot/v1/toAst/undefined.ts | 2 +- packages/openapi-ts/src/plugins/valibot/v1/toAst/unknown.ts | 2 +- packages/openapi-ts/src/plugins/valibot/v1/toAst/void.ts | 2 +- packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/valibot/default/valibot.gen.ts | 8 ++++---- packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/valibot/default/valibot.gen.ts | 164 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------------------------------------------------------------------- packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/valibot/default/valibot.gen.ts | 171 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------------------------------------------------------- 43 file(s) changed, 1357 insertion(s)(+), 743 deletion(s)(-) diff --git a/pyproject.toml b/pyproject.toml --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ requires-python = ">=3.9" [dependency-groups] -dev = ["httpx>=0.27", "mypy>=1.15", "ruff>=0.11"] +dev = ["httpx>=0.27", "mypy>=1.15", "pydantic>=2.0", "ruff>=0.11"] [tool.mypy] python_version = "3.9" diff --git a/uv.lock b/uv.lock --- a/uv.lock +++ b/uv.lock @@ -3,6 +3,15 @@ requires-python = ">=3.9" [[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] name = "anyio" version = "4.12.1" source = { registry = "https://pypi.org/simple" } @@ -55,6 +64,7 @@ dev = [ { name = "httpx" }, { name = "mypy" }, + { name = "pydantic" }, { name = "ruff" }, ] @@ -64,6 +74,7 @@ dev = [ { name = "httpx", specifier = ">=0.27" }, { name = "mypy", specifier = ">=1.15" }, + { name = "pydantic", specifier = ">=2.0" }, { name = "ruff", specifier = ">=0.11" }, ] @@ -258,6 +269,152 @@ ] [[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/54/db/160dffb57ed9a3705c4cbcbff0ac03bdae45f1ca7d58ab74645550df3fbd/pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf", size = 2107999, upload-time = "2025-11-04T13:42:03.885Z" }, + { url = "https://files.pythonhosted.org/packages/a3/7d/88e7de946f60d9263cc84819f32513520b85c0f8322f9b8f6e4afc938383/pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5", size = 1929745, upload-time = "2025-11-04T13:42:06.075Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c2/aef51e5b283780e85e99ff19db0f05842d2d4a8a8cd15e63b0280029b08f/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d", size = 1920220, upload-time = "2025-11-04T13:42:08.457Z" }, + { url = "https://files.pythonhosted.org/packages/c7/97/492ab10f9ac8695cd76b2fdb24e9e61f394051df71594e9bcc891c9f586e/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60", size = 2067296, upload-time = "2025-11-04T13:42:10.817Z" }, + { url = "https://files.pythonhosted.org/packages/ec/23/984149650e5269c59a2a4c41d234a9570adc68ab29981825cfaf4cfad8f4/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82", size = 2231548, upload-time = "2025-11-04T13:42:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/71/0c/85bcbb885b9732c28bec67a222dbed5ed2d77baee1f8bba2002e8cd00c5c/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5", size = 2362571, upload-time = "2025-11-04T13:42:16.208Z" }, + { url = "https://files.pythonhosted.org/packages/c0/4a/412d2048be12c334003e9b823a3fa3d038e46cc2d64dd8aab50b31b65499/pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3", size = 2068175, upload-time = "2025-11-04T13:42:18.911Z" }, + { url = "https://files.pythonhosted.org/packages/73/f4/c58b6a776b502d0a5540ad02e232514285513572060f0d78f7832ca3c98b/pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425", size = 2177203, upload-time = "2025-11-04T13:42:22.578Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ae/f06ea4c7e7a9eead3d165e7623cd2ea0cb788e277e4f935af63fc98fa4e6/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504", size = 2148191, upload-time = "2025-11-04T13:42:24.89Z" }, + { url = "https://files.pythonhosted.org/packages/c1/57/25a11dcdc656bf5f8b05902c3c2934ac3ea296257cc4a3f79a6319e61856/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5", size = 2343907, upload-time = "2025-11-04T13:42:27.683Z" }, + { url = "https://files.pythonhosted.org/packages/96/82/e33d5f4933d7a03327c0c43c65d575e5919d4974ffc026bc917a5f7b9f61/pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3", size = 2322174, upload-time = "2025-11-04T13:42:30.776Z" }, + { url = "https://files.pythonhosted.org/packages/81/45/4091be67ce9f469e81656f880f3506f6a5624121ec5eb3eab37d7581897d/pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460", size = 1990353, upload-time = "2025-11-04T13:42:33.111Z" }, + { url = "https://files.pythonhosted.org/packages/44/8a/a98aede18db6e9cd5d66bcacd8a409fcf8134204cdede2e7de35c5a2c5ef/pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b", size = 2015698, upload-time = "2025-11-04T13:42:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] name = "ruff" version = "0.14.14" source = { registry = "https://pypi.org/simple" } @@ -344,4 +501,16 @@ sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] diff --git a/.changeset/brown-ideas-matter.md b/.changeset/brown-ideas-matter.md new file mode 100644 --- /dev/null +++ b/.changeset/brown-ideas-matter.md @@ -0,0 +1,5 @@ +--- +"@hey-api/shared": patch +--- + +**internal**: export schema walker interfaces diff --git a/.changeset/clean-singers-stand.md b/.changeset/clean-singers-stand.md new file mode 100644 --- /dev/null +++ b/.changeset/clean-singers-stand.md @@ -0,0 +1,9 @@ +--- +"@hey-api/openapi-ts": minor +--- + +**plugin(valibot)**: remove `enum.nodes.nullable` resolver node + +### Removed resolver node + +Valibot plugin no longer exposes the `enum.nodes.nullable` node. It was refactored so that nullable values are handled outside of resolvers. diff --git a/.changeset/stale-falcons-raise.md b/.changeset/stale-falcons-raise.md new file mode 100644 --- /dev/null +++ b/.changeset/stale-falcons-raise.md @@ -0,0 +1,5 @@ +--- +"@hey-api/openapi-ts": patch +--- + +**plugin(valibot)**: use `.nullable()` and `.nullish()` methods diff --git a/docs/openapi-ts/migrating.md b/docs/openapi-ts/migrating.md --- a/docs/openapi-ts/migrating.md +++ b/docs/openapi-ts/migrating.md @@ -7,6 +7,12 @@ While we try to avoid breaking changes, sometimes it's unavoidable in order to offer you the latest features. This page lists changes that require updates to your code. If you run into a problem with migration, please [open an issue](https://github.com/hey-api/openapi-ts/issues). +## v0.93.0 + +### Removed resolver node + +Valibot and Zod plugins no longer expose the `enum.nodes.nullable` node. Both plugins were refactored so that nullable values are handled outside of resolvers. + ## v0.92.0 ### Updated Symbol interface diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -58,6 +58,15 @@ parameterWithPagination, } from './ir/parameter'; export { deduplicateSchema } from './ir/schema'; +export type { + SchemaExtractor, + SchemaProcessor, + SchemaProcessorContext, + SchemaProcessorResult, +} from './ir/schema-processor'; +export { createSchemaProcessor } from './ir/schema-processor'; +export type { SchemaResult, SchemaVisitor, SchemaVisitorContext, Walker } from './ir/schema-walker'; +export { childContext, createSchemaWalker } from './ir/schema-walker'; export type { IR } from './ir/types'; export { addItemsToSchema } from './ir/utils'; export { parseOpenApiSpec } from './openApi'; @@ -87,13 +96,6 @@ OpenApiSchemaObject, } from './openApi/types'; export type { Hooks } from './parser/hooks'; -export type { - SchemaExtractor, - SchemaProcessor, - SchemaProcessorContext, - SchemaProcessorResult, -} from './plugins/schema-processor'; -export { createSchemaProcessor } from './plugins/schema-processor'; export type { SchemaWithType } from './plugins/shared/types/schema'; export { definePluginConfig, mappers } from './plugins/shared/utils/config'; export type { PluginInstanceTypes } from './plugins/shared/utils/instance'; diff --git a/packages/shared/src/ir/schema-processor.ts b/packages/shared/src/ir/schema-processor.ts new file mode 100644 --- /dev/null +++ b/packages/shared/src/ir/schema-processor.ts @@ -0,0 +1,70 @@ +import { pathToJsonPointer } from '../utils/ref'; +import type { IR } from './types'; + +export interface SchemaProcessor { + /** Current inherited context (set by withContext) */ + readonly context: { + readonly anchor: string | undefined; + readonly tags: ReadonlyArray | undefined; + }; + /** Check if pointer was already emitted */ + hasEmitted: (path: ReadonlyArray) => boolean; + /** Mark pointer as emitted. Returns false if already emitted. */ + markEmitted: (path: ReadonlyArray) => boolean; + /** Execute with inherited context for nested extractions */ + withContext: (ctx: { anchor?: string; tags?: ReadonlyArray }, fn: () => T) => T; +} + +export interface SchemaProcessorContext { + meta: { resource: string; resourceId: string; role?: string }; + namingAnchor?: string; + path: ReadonlyArray; + schema: IR.SchemaObject; + tags?: ReadonlyArray; +} + +export interface SchemaProcessorResult< + Context extends SchemaProcessorContext = SchemaProcessorContext, +> { + process: (ctx: Context) => void; +} + +export type SchemaExtractor = ( + ctx: Context, +) => IR.SchemaObject; + +export function createSchemaProcessor(): SchemaProcessor { + const emitted = new Set(); + let contextTags: ReadonlyArray | undefined; + let contextAnchor: string | undefined; + + return { + get context() { + return { + anchor: contextAnchor, + tags: contextTags, + }; + }, + hasEmitted(path) { + return emitted.has(pathToJsonPointer(path)); + }, + markEmitted(path) { + const pointer = pathToJsonPointer(path); + if (emitted.has(pointer)) return false; + emitted.add(pointer); + return true; + }, + withContext(ctx, fn) { + const prevTags = contextTags; + const prevAnchor = contextAnchor; + contextTags = ctx.tags; + contextAnchor = ctx.anchor; + try { + return fn(); + } finally { + contextTags = prevTags; + contextAnchor = prevAnchor; + } + }, + }; +} diff --git a/packages/shared/src/ir/schema-walker.ts b/packages/shared/src/ir/schema-walker.ts new file mode 100644 --- /dev/null +++ b/packages/shared/src/ir/schema-walker.ts @@ -0,0 +1,283 @@ +import type { Ref } from '@hey-api/codegen-core'; +import { fromRef, ref } from '@hey-api/codegen-core'; + +import type { SchemaWithType } from '../plugins/shared/types/schema'; +import { deduplicateSchema } from './schema'; +import type { IR } from './types'; + +/** + * Result returned by visitor methods. Contains the expression plus metadata + * needed for modifier application. + */ +export interface SchemaResult { + /** Default value from schema, if any. */ + default?: unknown; + /** The core schema expression, WITHOUT optional/nullable/default applied. */ + expression: TExpr; + /** The original schema format. */ + format?: string; + /** For libraries that need lazy evaluation. */ + hasLazyExpression?: boolean; + /** Plugin-specific metadata bucket. */ + meta?: Record; + /** Does this schema explicitly allow null? */ + nullable: boolean; + /** Is this schema read-only? */ + readonly: boolean; +} + +/** + * Context passed to all visitor methods. + */ +export interface SchemaVisitorContext { + /** Current path in the schema tree. */ + path: Ref>; + /** The plugin instance. */ + plugin: TPlugin; +} + +/** + * The walk function signature. + */ +export type Walker = ( + schema: IR.SchemaObject, + ctx: SchemaVisitorContext, +) => SchemaResult; + +/** + * The visitor interface. Plugins implement this to define how schemas + * are transformed into their target representation. + */ +export interface SchemaVisitor { + /** + * Apply modifiers (optional/nullable/nullish/default) to a schema result. + */ + applyModifiers( + result: SchemaResult, + ctx: SchemaVisitorContext, + context?: { + /** Is this property optional? */ + optional?: boolean; + }, + ): TExpr; + array( + schema: SchemaWithType<'array'>, + ctx: SchemaVisitorContext, + walk: Walker, + ): SchemaResult; + boolean( + schema: SchemaWithType<'boolean'>, + ctx: SchemaVisitorContext, + ): SchemaResult; + enum( + schema: SchemaWithType<'enum'>, + ctx: SchemaVisitorContext, + walk: Walker, + ): SchemaResult; + integer( + schema: SchemaWithType<'integer'>, + ctx: SchemaVisitorContext, + ): SchemaResult; + /** + * Called before any dispatch logic. Return a result to short-circuit, + * or undefined to continue normal dispatch. + */ + intercept?( + schema: IR.SchemaObject, + ctx: SchemaVisitorContext, + walk: Walker, + ): SchemaResult | undefined; + /** + * Handle intersection types. Receives already-walked child results. + */ + intersection( + items: Array>, + schemas: ReadonlyArray, + ctx: SchemaVisitorContext, + ): SchemaResult; + never(schema: SchemaWithType<'never'>, ctx: SchemaVisitorContext): SchemaResult; + null(schema: SchemaWithType<'null'>, ctx: SchemaVisitorContext): SchemaResult; + number(schema: SchemaWithType<'number'>, ctx: SchemaVisitorContext): SchemaResult; + object( + schema: SchemaWithType<'object'>, + ctx: SchemaVisitorContext, + walk: Walker, + ): SchemaResult; + /** + * Called after each typed schema visitor returns. + */ + postProcess?( + result: SchemaResult, + schema: IR.SchemaObject, + ctx: SchemaVisitorContext, + ): SchemaResult; + /** + * Handle $ref to another schema. + */ + reference( + $ref: string, + schema: IR.SchemaObject, + ctx: SchemaVisitorContext, + ): SchemaResult; + string(schema: SchemaWithType<'string'>, ctx: SchemaVisitorContext): SchemaResult; + tuple( + schema: SchemaWithType<'tuple'>, + ctx: SchemaVisitorContext, + walk: Walker, + ): SchemaResult; + undefined( + schema: SchemaWithType<'undefined'>, + ctx: SchemaVisitorContext, + ): SchemaResult; + /** + * Handle union types. Receives already-walked child results. + */ + union( + items: Array>, + schemas: ReadonlyArray, + ctx: SchemaVisitorContext, + ): SchemaResult; + unknown( + schema: SchemaWithType<'unknown'>, + ctx: SchemaVisitorContext, + ): SchemaResult; + void(schema: SchemaWithType<'void'>, ctx: SchemaVisitorContext): SchemaResult; +} + +/** + * Create a schema walker from a visitor. + * + * The walker handles: + * - Dispatch order ($ref → type → items → fallback) + * - Deduplication of union/intersection schemas + * - Path tracking for child schemas + * - Calling the appropriate visitor method + */ +export function createSchemaWalker( + visitor: SchemaVisitor, +): Walker { + const walk: Walker = (schema, ctx) => { + // escape hatch + if (visitor.intercept) { + const intercepted = visitor.intercept(schema, ctx, walk); + if (intercepted !== undefined) { + return intercepted; + } + } + + const baseResult = { + default: schema.default, + readonly: schema.accessScope === 'read', + }; + + if (schema.$ref) { + const result = visitor.reference(schema.$ref, schema, ctx); + return { + ...result, + default: result.default ?? baseResult.default, + readonly: result.readonly || baseResult.readonly, + }; + } + + if (schema.type) { + let result = visitTyped(schema as SchemaWithType, ctx, visitor, walk); + if (visitor.postProcess) { + result = visitor.postProcess(result, schema, ctx); + } + return { + ...result, + default: result.default ?? baseResult.default, + readonly: result.readonly || baseResult.readonly, + }; + } + + if (schema.items) { + const deduplicated = deduplicateSchema({ schema }); + + // deduplication might collapse to a single schema + if (!deduplicated.items) { + return walk(deduplicated, ctx); + } + + const itemResults = deduplicated.items.map((item, index) => + walk(item, { + ...ctx, + path: ref([...fromRef(ctx.path), 'items', index]), + }), + ); + + const result = + deduplicated.logicalOperator === 'and' + ? visitor.intersection(itemResults, deduplicated.items, ctx) + : visitor.union(itemResults, deduplicated.items, ctx); + + return { + ...result, + default: result.default ?? baseResult.default, + readonly: result.readonly || baseResult.readonly, + }; + } + + // fallback + const result = visitor.unknown({ type: 'unknown' }, ctx); + return { + ...result, + default: result.default ?? baseResult.default, + readonly: result.readonly || baseResult.readonly, + }; + }; + + return walk; +} + +/** + * Dispatch to the appropriate visitor method based on schema type. + */ +function visitTyped( + schema: SchemaWithType, + ctx: SchemaVisitorContext, + visitor: SchemaVisitor, + walk: Walker, +): SchemaResult { + switch (schema.type) { + case 'array': + return visitor.array(schema as SchemaWithType<'array'>, ctx, walk); + case 'boolean': + return visitor.boolean(schema as SchemaWithType<'boolean'>, ctx); + case 'enum': + return visitor.enum(schema as SchemaWithType<'enum'>, ctx, walk); + case 'integer': + return visitor.integer(schema as SchemaWithType<'integer'>, ctx); + case 'never': + return visitor.never(schema as SchemaWithType<'never'>, ctx); + case 'null': + return visitor.null(schema as SchemaWithType<'null'>, ctx); + case 'number': + return visitor.number(schema as SchemaWithType<'number'>, ctx); + case 'object': + return visitor.object(schema as SchemaWithType<'object'>, ctx, walk); + case 'string': + return visitor.string(schema as SchemaWithType<'string'>, ctx); + case 'tuple': + return visitor.tuple(schema as SchemaWithType<'tuple'>, ctx, walk); + case 'undefined': + return visitor.undefined(schema as SchemaWithType<'undefined'>, ctx); + case 'unknown': + return visitor.unknown(schema as SchemaWithType<'unknown'>, ctx); + case 'void': + return visitor.void(schema as SchemaWithType<'void'>, ctx); + } +} + +/** + * Helper to create a child context with an extended path. + */ +export function childContext( + ctx: SchemaVisitorContext, + ...segments: ReadonlyArray +): SchemaVisitorContext { + return { + ...ctx, + path: ref([...fromRef(ctx.path), ...segments]), + }; +} diff --git a/packages/shared/src/parser/hooks.ts b/packages/shared/src/parser/hooks.ts --- a/packages/shared/src/parser/hooks.ts +++ b/packages/shared/src/parser/hooks.ts @@ -1,7 +1,7 @@ import type { Node, Symbol, SymbolIn } from '@hey-api/codegen-core'; +import type { SchemaProcessorContext } from '../ir/schema-processor'; import type { IROperationObject } from '../ir/types'; -import type { SchemaProcessorContext } from '../plugins/schema-processor'; import type { PluginInstance } from '../plugins/shared/utils/instance'; export type Hooks = { diff --git a/packages/shared/src/plugins/schema-processor.ts b/packages/shared/src/plugins/schema-processor.ts deleted file mode 100644 --- a/packages/shared/src/plugins/schema-processor.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { IR } from '../ir/types'; -import { pathToJsonPointer } from '../utils/ref'; - -export interface SchemaProcessor { - /** Current inherited context (set by withContext) */ - readonly context: { - readonly anchor: string | undefined; - readonly tags: ReadonlyArray | undefined; - }; - /** Check if pointer was already emitted */ - hasEmitted: (path: ReadonlyArray) => boolean; - /** Mark pointer as emitted. Returns false if already emitted. */ - markEmitted: (path: ReadonlyArray) => boolean; - /** Execute with inherited context for nested extractions */ - withContext: (ctx: { anchor?: string; tags?: ReadonlyArray }, fn: () => T) => T; -} - -export interface SchemaProcessorContext { - meta: { resource: string; resourceId: string; role?: string }; - namingAnchor?: string; - path: ReadonlyArray; - schema: IR.SchemaObject; - tags?: ReadonlyArray; -} - -export interface SchemaProcessorResult< - Context extends SchemaProcessorContext = SchemaProcessorContext, -> { - process: (ctx: Context) => void; -} - -export type SchemaExtractor = ( - ctx: Context, -) => IR.SchemaObject; - -export function createSchemaProcessor(): SchemaProcessor { - const emitted = new Set(); - let contextTags: ReadonlyArray | undefined; - let contextAnchor: string | undefined; - - return { - get context() { - return { - anchor: contextAnchor, - tags: contextTags, - }; - }, - hasEmitted(path) { - return emitted.has(pathToJsonPointer(path)); - }, - markEmitted(path) { - const pointer = pathToJsonPointer(path); - if (emitted.has(pointer)) return false; - emitted.add(pointer); - return true; - }, - withContext(ctx, fn) { - const prevTags = contextTags; - const prevAnchor = contextAnchor; - contextTags = ctx.tags; - contextAnchor = ctx.anchor; - try { - return fn(); - } finally { - contextTags = prevTags; - contextAnchor = prevAnchor; - } - }, - }; -} diff --git a/packages/openapi-ts/src/plugins/zod/constants.ts b/packages/openapi-ts/src/plugins/zod/constants.ts --- a/packages/openapi-ts/src/plugins/zod/constants.ts +++ b/packages/openapi-ts/src/plugins/zod/constants.ts @@ -39,6 +39,7 @@ never: 'never', null: 'null', nullable: 'nullable', + nullish: 'nullish', number: 'number', object: 'object', optional: 'optional', diff --git a/packages/openapi-python/src/plugins/pydantic/shared/export.ts b/packages/openapi-python/src/plugins/pydantic/shared/export.ts --- a/packages/openapi-python/src/plugins/pydantic/shared/export.ts +++ b/packages/openapi-python/src/plugins/pydantic/shared/export.ts @@ -1,25 +1,36 @@ -import type { Symbol } from '@hey-api/codegen-core'; -import type { IR } from '@hey-api/shared'; +import { applyNaming, pathToName } from '@hey-api/shared'; // import { createSchemaComment } from '../../../plugins/shared/utils/schema'; import { $ } from '../../../py-dsl'; +import type { ProcessorContext } from './processor'; // import { identifiers } from '../v2/constants'; // import { pipesToNode } from './pipes'; import type { Ast, IrSchemaToAstOptions } from './types'; export function exportAst({ - // ast, + meta, + naming, + namingAnchor, + path, plugin, - // schema, - // state, - symbol, -}: IrSchemaToAstOptions & { - ast: Ast; - schema: IR.SchemaObject; - symbol: Symbol; -}): void { - // const v = plugin.external('valibot.v'); - const classDef = $.class(symbol); + tags, +}: Pick & + ProcessorContext & { + ast: Ast; + }): void { + const name = pathToName(path, { anchor: namingAnchor }); + const symbol = plugin.symbol(applyNaming(name, naming), { + meta: { + category: 'schema', + path, + tags, + tool: 'pydantic', + ...meta, + }, + }); + + const baseModel = plugin.external('pydantic.BaseModel'); + const classDef = $.class(symbol).extends(baseModel); // .export() // .$if(plugin.config.comments && createSchemaComment(schema), (c, v) => c.doc(v)) // .$if(state.hasLazyExpression['~ref'], (c) => diff --git a/packages/openapi-python/src/plugins/pydantic/shared/processor.ts b/packages/openapi-python/src/plugins/pydantic/shared/processor.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-python/src/plugins/pydantic/shared/processor.ts @@ -0,0 +1,16 @@ +import type { + IR, + NamingConfig, + SchemaProcessorContext, + SchemaProcessorResult, +} from '@hey-api/shared'; + +import type { IrSchemaToAstOptions } from './types'; + +export type ProcessorContext = Pick & + SchemaProcessorContext & { + naming: NamingConfig; + schema: IR.SchemaObject; + }; + +export type ProcessorResult = SchemaProcessorResult; diff --git a/packages/openapi-python/src/plugins/pydantic/shared/types.ts b/packages/openapi-python/src/plugins/pydantic/shared/types.ts --- a/packages/openapi-python/src/plugins/pydantic/shared/types.ts +++ b/packages/openapi-python/src/plugins/pydantic/shared/types.ts @@ -3,6 +3,7 @@ import type { $ } from '../../../py-dsl'; import type { PydanticPlugin } from '../types'; +import type { ProcessorContext } from './processor'; export type Ast = { /** @@ -32,7 +33,7 @@ /** The plugin instance. */ plugin: PydanticPlugin['Instance']; /** Optional schema extractor function. */ - schemaExtractor?: SchemaExtractor; + schemaExtractor?: SchemaExtractor; /** The plugin state references. */ state: Refs; }; diff --git a/packages/openapi-python/src/plugins/pydantic/v2/plugin.ts b/packages/openapi-python/src/plugins/pydantic/v2/plugin.ts --- a/packages/openapi-python/src/plugins/pydantic/v2/plugin.ts +++ b/packages/openapi-python/src/plugins/pydantic/v2/plugin.ts @@ -1,12 +1,12 @@ import type { SymbolMeta } from '@hey-api/codegen-core'; -import { fromRef, ref, refs } from '@hey-api/codegen-core'; +import { fromRef, ref } from '@hey-api/codegen-core'; import type { IR, SchemaWithType } from '@hey-api/shared'; -import { applyNaming, deduplicateSchema, pathToJsonPointer, refToName } from '@hey-api/shared'; +import { deduplicateSchema, pathToJsonPointer } from '@hey-api/shared'; // import { $ } from '../../../py-dsl'; -import { exportAst } from '../shared/export'; -import type { Ast, IrSchemaToAstOptions, PluginState } from '../shared/types'; +import type { Ast, IrSchemaToAstOptions } from '../shared/types'; import type { PydanticPlugin } from '../types'; +import { createProcessor } from './processor'; import { irSchemaWithTypeToAst } from './toAst'; export function irSchemaToAst({ @@ -25,7 +25,9 @@ resource: 'definition', resourceId: pathToJsonPointer(fromRef(state.path)), }, + naming: plugin.config.definitions, path: fromRef(state.path), + plugin, schema, }); if (extracted !== schema) schema = extracted; @@ -119,39 +121,6 @@ }; } -function handleComponent({ - plugin, - schema, - state, -}: IrSchemaToAstOptions & { - schema: IR.SchemaObject; -}): void { - const $ref = pathToJsonPointer(fromRef(state.path)); - const baseName = refToName($ref); - const symbol = plugin.symbol(applyNaming(baseName, plugin.config.definitions), { - meta: { - category: 'schema', - path: fromRef(state.path), - resource: 'definition', - resourceId: $ref, - tags: fromRef(state.tags), - tool: 'pydantic', - }, - }); - const ast = irSchemaToAst({ - plugin, - schema, - state, - }); - exportAst({ - ast, - plugin, - schema, - state, - symbol, - }); -} - export const handlerV2: PydanticPlugin['Handler'] = ({ plugin }) => { plugin.symbol('Any', { external: 'typing', @@ -202,36 +171,47 @@ }, }); - plugin.forEach('operation', 'parameter', 'requestBody', 'schema', 'webhook', (event) => { - const state = refs({ - hasLazyExpression: false, - path: event._path, - tags: event.tags, - }); + const processor = createProcessor(plugin); + plugin.forEach('operation', 'parameter', 'requestBody', 'schema', 'webhook', (event) => { switch (event.type) { case 'parameter': - handleComponent({ - // baseName: event.name, + processor.process({ + meta: { + resource: 'definition', + resourceId: pathToJsonPointer(event._path), + }, + naming: plugin.config.definitions, + path: event._path, plugin, schema: event.parameter.schema, - state, + tags: event.tags, }); break; case 'requestBody': - handleComponent({ - // baseName: event.name, + processor.process({ + meta: { + resource: 'definition', + resourceId: pathToJsonPointer(event._path), + }, + naming: plugin.config.definitions, + path: event._path, plugin, schema: event.requestBody.schema, - state, + tags: event.tags, }); break; case 'schema': - handleComponent({ - // baseName: event.name, + processor.process({ + meta: { + resource: 'definition', + resourceId: pathToJsonPointer(event._path), + }, + naming: plugin.config.definitions, + path: event._path, plugin, schema: event.schema, - state, + tags: event.tags, }); break; } diff --git a/packages/openapi-python/src/plugins/pydantic/v2/processor.ts b/packages/openapi-python/src/plugins/pydantic/v2/processor.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-python/src/plugins/pydantic/v2/processor.ts @@ -0,0 +1,58 @@ +import { refs } from '@hey-api/codegen-core'; +import type { IR } from '@hey-api/shared'; +import { createSchemaProcessor, pathToJsonPointer } from '@hey-api/shared'; + +import { exportAst } from '../shared/export'; +import type { ProcessorContext, ProcessorResult } from '../shared/processor'; +import type { PluginState } from '../shared/types'; +import type { PydanticPlugin } from '../types'; +import { irSchemaToAst } from './plugin'; + +export function createProcessor(plugin: PydanticPlugin['Instance']): ProcessorResult { + const processor = createSchemaProcessor(); + + const hooks = [plugin.config['~hooks']?.schemas, plugin.context.config.parser.hooks.schemas]; + + function extractor(ctx: ProcessorContext): IR.SchemaObject { + if (processor.hasEmitted(ctx.path)) { + return ctx.schema; + } + + for (const hook of hooks) { + const result = hook?.shouldExtract?.(ctx); + if (result) { + process({ + namingAnchor: processor.context.anchor, + tags: processor.context.tags, + ...ctx, + }); + return { $ref: pathToJsonPointer(ctx.path) }; + } + } + + return ctx.schema; + } + + function process(ctx: ProcessorContext): void { + if (!processor.markEmitted(ctx.path)) return; + + processor.withContext({ anchor: ctx.namingAnchor, tags: ctx.tags }, () => { + const state = refs({ + hasLazyExpression: false, + path: ctx.path, + tags: ctx.tags, + }); + + const ast = irSchemaToAst({ + plugin, + schema: ctx.schema, + schemaExtractor: extractor, + state, + }); + + exportAst({ ...ctx, ast, plugin, state }); + }); + } + + return { process }; +} diff --git a/packages/openapi-ts/src/plugins/valibot/resolvers/types.ts b/packages/openapi-ts/src/plugins/valibot/resolvers/types.ts --- a/packages/openapi-ts/src/plugins/valibot/resolvers/types.ts +++ b/packages/openapi-ts/src/plugins/valibot/resolvers/types.ts @@ -1,11 +1,19 @@ import type { Refs, Symbol } from '@hey-api/codegen-core'; -import type { IR, Plugin, SchemaWithType } from '@hey-api/shared'; +import type { + IR, + Plugin, + SchemaResult, + SchemaVisitorContext, + SchemaWithType, + Walker, +} from '@hey-api/shared'; import type { MaybeBigInt, ShouldCoerceToBigInt } from '../../../plugins/shared/utils/coerce'; import type { GetIntegerLimit } from '../../../plugins/shared/utils/formats'; import type { $, DollarTsDsl } from '../../../ts-dsl'; import type { Pipe, PipeResult, Pipes, PipesUtils } from '../shared/pipes'; import type { Ast, IrSchemaToAstOptions, PluginState } from '../shared/types'; +import type { ValibotPlugin } from '../types'; export type Resolvers = Plugin.Resolvers<{ /** @@ -70,7 +78,7 @@ type ValidatorResolver = (ctx: ValidatorResolverContext) => PipeResult | null | undefined; type BaseContext = DollarTsDsl & - Pick & { + Pick & { /** * Functions for working with pipes. */ @@ -116,10 +124,6 @@ */ isNullable: boolean; }; - /** - * Returns a nullable wrapper if the enum includes null, undefined otherwise. - */ - nullable: (ctx: EnumResolverContext) => PipeResult | undefined; }; schema: SchemaWithType<'enum'>; /** @@ -152,6 +156,7 @@ } export interface ObjectResolverContext extends BaseContext { + applyModifiers: (result: SchemaResult, opts: { optional?: boolean }) => Ast; /** * Nodes used to build different parts of the object schema. */ @@ -172,6 +177,8 @@ ast: Partial>; state: Refs; }; + walk: Walker; + walkerCtx: SchemaVisitorContext; } export interface StringResolverContext extends BaseContext { diff --git a/packages/openapi-ts/src/plugins/valibot/shared/types.ts b/packages/openapi-ts/src/plugins/valibot/shared/types.ts --- a/packages/openapi-ts/src/plugins/valibot/shared/types.ts +++ b/packages/openapi-ts/src/plugins/valibot/shared/types.ts @@ -1,10 +1,9 @@ import type { Refs, SymbolMeta } from '@hey-api/codegen-core'; -import type { IR, SchemaExtractor } from '@hey-api/shared'; +import type { IR, Walker } from '@hey-api/shared'; import type ts from 'typescript'; import type { ValibotPlugin } from '../types'; import type { Pipes } from './pipes'; -import type { ProcessorContext } from './processor'; export type Ast = { hasLazyExpression?: boolean; @@ -15,10 +14,9 @@ export type IrSchemaToAstOptions = { /** The plugin instance. */ plugin: ValibotPlugin['Instance']; - /** Optional schema extractor function. */ - schemaExtractor?: SchemaExtractor; /** The plugin state references. */ state: Refs; + walk: Walker; }; export type PluginState = Pick, 'path'> & diff --git a/packages/openapi-ts/src/plugins/valibot/v1/plugin.ts b/packages/openapi-ts/src/plugins/valibot/v1/plugin.ts --- a/packages/openapi-ts/src/plugins/valibot/v1/plugin.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/plugin.ts @@ -1,158 +1,9 @@ -import type { SymbolMeta } from '@hey-api/codegen-core'; -import { fromRef, ref } from '@hey-api/codegen-core'; -import type { IR, SchemaWithType } from '@hey-api/shared'; -import { deduplicateSchema, pathToJsonPointer } from '@hey-api/shared'; +import { pathToJsonPointer } from '@hey-api/shared'; -import { maybeBigInt } from '../../../plugins/shared/utils/coerce'; -import { $ } from '../../../ts-dsl'; import { irOperationToAst } from '../shared/operation'; -import { pipesToNode } from '../shared/pipes'; -import type { Ast, IrSchemaToAstOptions } from '../shared/types'; import { irWebhookToAst } from '../shared/webhook'; import type { ValibotPlugin } from '../types'; -import { identifiers } from './constants'; import { createProcessor } from './processor'; -import { irSchemaWithTypeToAst } from './toAst'; - -export function irSchemaToAst({ - optional, - plugin, - schema, - schemaExtractor, - state, -}: IrSchemaToAstOptions & { - /** - * Accept `optional` to handle optional object properties. We can't handle - * this inside the object function because `.optional()` must come before - * `.default()` which is handled in this function. - */ - optional?: boolean; - schema: IR.SchemaObject; -}): Ast { - if (schemaExtractor && !schema.$ref) { - const extracted = schemaExtractor({ - meta: { - resource: 'definition', - resourceId: pathToJsonPointer(fromRef(state.path)), - }, - naming: plugin.config.definitions, - path: fromRef(state.path), - plugin, - schema, - }); - if (extracted !== schema) schema = extracted; - } - - const ast: Ast = { - pipes: [], - }; - - const v = plugin.external('valibot.v'); - - if (schema.$ref) { - const query: SymbolMeta = { - category: 'schema', - resource: 'definition', - resourceId: schema.$ref, - tool: 'valibot', - }; - const refSymbol = plugin.referenceSymbol(query); - if (plugin.isSymbolRegistered(query)) { - const ref = $(refSymbol); - ast.pipes.push(ref); - } else { - const lazyExpression = $(v) - .attr(identifiers.schemas.lazy) - .call($.func().do($(refSymbol).return())); - ast.pipes.push(lazyExpression); - state.hasLazyExpression['~ref'] = true; - } - } else if (schema.type) { - const typeAst = irSchemaWithTypeToAst({ - plugin, - schema: schema as SchemaWithType, - schemaExtractor, - state, - }); - ast.typeName = typeAst.anyType; - ast.pipes.push(typeAst.expression); - - if (plugin.config.metadata && schema.description) { - const expression = $(v) - .attr(identifiers.actions.metadata) - .call($.object().prop('description', $.literal(schema.description))); - ast.pipes.push(expression); - } - } else if (schema.items) { - schema = deduplicateSchema({ schema }); - - if (schema.items) { - const itemsAst = schema.items.map((item, index) => { - const itemAst = irSchemaToAst({ - plugin, - schema: item, - schemaExtractor, - state: { - ...state, - path: ref([...fromRef(state.path), 'items', index]), - }, - }); - return pipesToNode(itemAst.pipes, plugin); - }); - - if (schema.logicalOperator === 'and') { - const intersectExpression = $(v) - .attr(identifiers.schemas.intersect) - .call($.array(...itemsAst)); - ast.pipes.push(intersectExpression); - } else { - const unionExpression = $(v) - .attr(identifiers.schemas.union) - .call($.array(...itemsAst)); - ast.pipes.push(unionExpression); - } - } else { - const schemaPipes = irSchemaToAst({ plugin, schema, state }); - ast.pipes.push(...schemaPipes.pipes); - } - } else { - // catch-all fallback for failed schemas - const typeAst = irSchemaWithTypeToAst({ - plugin, - schema: { - type: 'unknown', - }, - schemaExtractor, - state, - }); - ast.typeName = typeAst.anyType; - ast.pipes.push(typeAst.expression); - } - - if (ast.pipes.length) { - if (schema.accessScope === 'read') { - const readonlyExpression = $(v).attr(identifiers.actions.readonly).call(); - ast.pipes.push(readonlyExpression); - } - - if (schema.default !== undefined) { - ast.pipes = [ - $(v) - .attr(identifiers.schemas.optional) - .call( - pipesToNode(ast.pipes, plugin), - schema.type === 'integer' || schema.type === 'number' - ? maybeBigInt(schema.default, schema.format) - : $.fromValue(schema.default), - ), - ]; - } else if (optional) { - ast.pipes = [$(v).attr(identifiers.schemas.optional).call(pipesToNode(ast.pipes, plugin))]; - } - } - - return ast as Ast; -} export const handlerV1: ValibotPlugin['Handler'] = ({ plugin }) => { plugin.symbol('v', { diff --git a/packages/openapi-ts/src/plugins/valibot/v1/processor.ts b/packages/openapi-ts/src/plugins/valibot/v1/processor.ts --- a/packages/openapi-ts/src/plugins/valibot/v1/processor.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/processor.ts @@ -1,12 +1,12 @@ -import { refs } from '@hey-api/codegen-core'; +import { ref, refs } from '@hey-api/codegen-core'; import type { IR } from '@hey-api/shared'; -import { createSchemaProcessor, pathToJsonPointer } from '@hey-api/shared'; +import { createSchemaProcessor, createSchemaWalker, pathToJsonPointer } from '@hey-api/shared'; import { exportAst } from '../shared/export'; import type { ProcessorContext, ProcessorResult } from '../shared/processor'; import type { PluginState } from '../shared/types'; import type { ValibotPlugin } from '../types'; -import { irSchemaToAst } from './plugin'; +import { createVisitor } from './walker'; export function createProcessor(plugin: ValibotPlugin['Instance']): ProcessorResult { const processor = createSchemaProcessor(); @@ -43,12 +43,24 @@ tags: ctx.tags, }); - const ast = irSchemaToAst({ - plugin, - schema: ctx.schema, + const visitor = createVisitor({ schemaExtractor: extractor, state, }); + const walk = createSchemaWalker(visitor); + + const result = walk(ctx.schema, { + path: ref(ctx.path), + plugin, + }); + const ast = + visitor.applyModifiers(result, { + path: ref(ctx.path), + plugin, + }) ?? result.expression; + if (result.hasLazyExpression) { + state.hasLazyExpression['~ref'] = true; + } exportAst({ ...ctx, ast, plugin, state }); }); diff --git a/packages/openapi-ts/src/plugins/valibot/v1/walker.ts b/packages/openapi-ts/src/plugins/valibot/v1/walker.ts new file mode 100644 --- /dev/null +++ b/packages/openapi-ts/src/plugins/valibot/v1/walker.ts @@ -0,0 +1,358 @@ +import type { Refs, SymbolMeta } from '@hey-api/codegen-core'; +import { fromRef } from '@hey-api/codegen-core'; +import type { IR, SchemaExtractor, SchemaResult, SchemaVisitor } from '@hey-api/shared'; +import { pathToJsonPointer } from '@hey-api/shared'; + +import { $ } from '../../../ts-dsl'; +import { maybeBigInt, shouldCoerceToBigInt } from '../../shared/utils/coerce'; +import { pipesToNode } from '../shared/pipes'; +import type { ProcessorContext } from '../shared/processor'; +import type { Ast, PluginState } from '../shared/types'; +import type { ValibotPlugin } from '../types'; +import { identifiers } from './constants'; +import { arrayToAst } from './toAst/array'; +import { booleanToAst } from './toAst/boolean'; +import { enumToAst } from './toAst/enum'; +import { neverToAst } from './toAst/never'; +import { nullToAst } from './toAst/null'; +import { numberToNode } from './toAst/number'; +import { objectToAst } from './toAst/object'; +import { stringToNode } from './toAst/string'; +import { tupleToAst } from './toAst/tuple'; +import { undefinedToAst } from './toAst/undefined'; +import { unknownToAst } from './toAst/unknown'; +import { voidToAst } from './toAst/void'; + +export interface VisitorConfig { + /** Optional schema extractor function. */ + schemaExtractor?: SchemaExtractor; + /** The plugin state references. */ + state: Refs; +} + +function getDefaultValue(result: SchemaResult) { + return result.format ? maybeBigInt(result.default, result.format) : $.fromValue(result.default); +} + +export function createVisitor( + config: VisitorConfig, +): SchemaVisitor { + const { schemaExtractor, state } = config; + return { + applyModifiers(result, ctx, options = {}) { + const { optional } = options; + const v = ctx.plugin.external('valibot.v'); + const pipes = [...result.expression.pipes]; + + if (result.readonly) { + pipes.push($(v).attr(identifiers.actions.readonly).call()); + } + + const hasDefault = result.default !== undefined; + const needsOptional = optional || hasDefault; + const needsNullable = result.nullable; + const innerNode = pipesToNode(pipes, ctx.plugin); + + if (needsOptional && needsNullable) { + if (hasDefault) { + return { + pipes: [ + $(v).attr(identifiers.schemas.nullish).call(innerNode, getDefaultValue(result)), + ], + }; + } + return { pipes: [$(v).attr(identifiers.schemas.nullish).call(innerNode)] }; + } + + if (needsOptional) { + if (hasDefault) { + return { + pipes: [ + $(v).attr(identifiers.schemas.optional).call(innerNode, getDefaultValue(result)), + ], + }; + } + return { pipes: [$(v).attr(identifiers.schemas.optional).call(innerNode)] }; + } + + if (needsNullable) { + return { pipes: [$(v).attr(identifiers.schemas.nullable).call(innerNode)] }; + } + + return { pipes }; + }, + array(schema, ctx, walk) { + const applyModifiers = (result: SchemaResult, opts: { optional?: boolean }) => + this.applyModifiers(result, ctx, opts); + const ast = arrayToAst({ + ...ctx, + applyModifiers, + schema, + state, + walk, + }); + return { + expression: ast, + hasLazyExpression: state.hasLazyExpression['~ref'], + nullable: false, + readonly: false, + }; + }, + boolean(schema, ctx) { + const pipe = booleanToAst({ ...ctx, schema }); + return { + expression: { pipes: [pipe] }, + nullable: false, + readonly: false, + }; + }, + enum(schema, ctx) { + const pipe = enumToAst({ ...ctx, schema, state }); + const hasNull = + schema.items?.some((item) => item.type === 'null' || item.const === null) ?? false; + return { + expression: { pipes: [pipe] }, + nullable: hasNull, + readonly: false, + }; + }, + integer(schema, ctx) { + const pipe = numberToNode({ ...ctx, schema }); + return { + expression: { pipes: [pipe] }, + format: schema.format, + nullable: false, + readonly: false, + }; + }, + intercept(schema, ctx, walk) { + if (schemaExtractor && !schema.$ref) { + const extracted = schemaExtractor({ + meta: { + resource: 'definition', + resourceId: pathToJsonPointer(fromRef(ctx.path)), + }, + naming: ctx.plugin.config.definitions, + path: fromRef(ctx.path), + plugin: ctx.plugin, + schema, + }); + if (extracted !== schema) { + return walk(extracted, ctx); + } + } + }, + intersection(items, schemas, ctx) { + const v = ctx.plugin.external('valibot.v'); + const hasAnyLazy = items.some((item) => item.hasLazyExpression); + const itemNodes = items.map((item) => pipesToNode(item.expression.pipes, ctx.plugin)); + + return { + expression: { + pipes: [ + $(v) + .attr(identifiers.schemas.intersect) + .call($.array(...itemNodes)), + ], + }, + hasLazyExpression: hasAnyLazy, + nullable: items.some((i) => i.nullable), + readonly: items.some((i) => i.readonly), + }; + }, + never(schema, ctx) { + const pipe = neverToAst({ ...ctx, schema }); + return { + expression: { pipes: [pipe] }, + nullable: false, + readonly: false, + }; + }, + null(schema, ctx) { + const pipe = nullToAst({ ...ctx, schema }); + return { + expression: { pipes: [pipe] }, + nullable: false, + readonly: false, + }; + }, + number(schema, ctx) { + const pipe = numberToNode({ ...ctx, schema }); + return { + expression: { pipes: [pipe] }, + format: schema.format, + nullable: false, + readonly: false, + }; + }, + object(schema, ctx, walk) { + const applyModifiers = (result: SchemaResult, opts: { optional?: boolean }) => + this.applyModifiers(result, ctx, opts); + const ast = objectToAst({ + applyModifiers, + plugin: ctx.plugin, + schema, + state, + walk, + walkerCtx: ctx, + }); + return { + expression: ast, + hasLazyExpression: ast.hasLazyExpression ?? false, + nullable: false, + readonly: false, + }; + }, + postProcess(result, schema, ctx) { + if (ctx.plugin.config.metadata && schema.description) { + const v = ctx.plugin.external('valibot.v'); + const metadataExpr = $(v) + .attr(identifiers.actions.metadata) + .call($.object().prop('description', $.literal(schema.description))); + return { + ...result, + expression: { pipes: [...result.expression.pipes, metadataExpr] }, + }; + } + return result; + }, + reference($ref, schema, ctx) { + const v = ctx.plugin.external('valibot.v'); + const query: SymbolMeta = { + category: 'schema', + resource: 'definition', + resourceId: $ref, + tool: 'valibot', + }; + + const refSymbol = ctx.plugin.referenceSymbol(query); + + if (ctx.plugin.isSymbolRegistered(query)) { + return { + expression: { pipes: [$(refSymbol)] }, + nullable: false, + readonly: false, + }; + } + + state.hasLazyExpression['~ref'] = true; + return { + expression: { + pipes: [ + $(v) + .attr(identifiers.schemas.lazy) + .call($.func().do($(refSymbol).return())), + ], + }, + hasLazyExpression: true, + nullable: false, + readonly: false, + }; + }, + string(schema, ctx) { + if (shouldCoerceToBigInt(schema.format)) { + const pipe = numberToNode({ + plugin: ctx.plugin, + schema: { ...schema, type: 'number' }, + }); + return { + expression: { pipes: [pipe] }, + nullable: false, + readonly: false, + }; + } + + const pipe = stringToNode({ ...ctx, schema }); + return { + expression: { pipes: [pipe] }, + nullable: false, + readonly: false, + }; + }, + tuple(schema, ctx, walk) { + const applyModifiers = (result: SchemaResult, opts: { optional?: boolean }) => + this.applyModifiers(result, ctx, opts); + const ast = tupleToAst({ + ...ctx, + applyModifiers, + schema, + state, + walk, + }); + return { + expression: ast, + hasLazyExpression: state.hasLazyExpression['~ref'], + nullable: false, + readonly: false, + }; + }, + undefined(schema, ctx) { + const pipe = undefinedToAst({ ...ctx, schema }); + return { + expression: { pipes: [pipe] }, + nullable: false, + readonly: false, + }; + }, + union(items, schemas, ctx) { + const v = ctx.plugin.external('valibot.v'); + const hasAnyLazy = items.some((item) => item.hasLazyExpression); + + const hasNull = schemas.some((s) => s.type === 'null') || items.some((i) => i.nullable); + + const nonNullItems: typeof items = []; + const nonNullSchemas: Array = []; + + items.forEach((item, index) => { + const schema = schemas[index]!; + if (schema.type !== 'null') { + nonNullItems.push(item); + nonNullSchemas.push(schema); + } + }); + + // Build union expression WITHOUT null - null is tracked via nullable flag + let expression: Ast; + if (nonNullItems.length === 0) { + // Union was only null + expression = { pipes: [$(v).attr(identifiers.schemas.null).call()] }; + } else if (nonNullItems.length === 1) { + // Single non-null item + expression = nonNullItems[0]!.expression; + } else { + // Multiple non-null items + const itemNodes = nonNullItems.map((i) => pipesToNode(i.expression.pipes, ctx.plugin)); + expression = { + pipes: [ + $(v) + .attr(identifiers.schemas.union) + .call($.array(...itemNodes)), + ], + }; + } + + return { + expression, + hasLazyExpression: hasAnyLazy, + nullable: hasNull, + readonly: false, + }; + }, + unknown(schema, ctx) { + const pipe = unknownToAst({ ...ctx, schema }); + return { + expression: { pipes: [pipe] }, + nullable: false, + readonly: false, + }; + }, + void(schema, ctx) { + const pipe = voidToAst({ ...ctx, schema }); + return { + expression: { pipes: [pipe] }, + nullable: false, + readonly: false, + }; + }, + }; +} diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/validators/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/validators/valibot.gen.ts --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/validators/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/validators/valibot.gen.ts @@ -4,12 +4,12 @@ export const vBaz = v.optional(v.pipe(v.pipe(v.string(), v.regex(/foo\nbar/)), v.readonly()), 'baz'); -export const vFoo: v.GenericSchema = v.optional(v.union([v.object({ - foo: v.optional(v.pipe(v.string(), v.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: v.optional(v.lazy(() => vBar)), - baz: v.optional(v.array(v.lazy(() => vFoo))), - qux: v.optional(v.pipe(v.number(), v.integer(), v.gtValue(0)), 0) - }), v.null()]), null); +export const vFoo: v.GenericSchema = v.nullish(v.object({ + foo: v.optional(v.pipe(v.string(), v.regex(/^\d{3}-\d{2}-\d{4}$/))), + bar: v.optional(v.lazy(() => vBar)), + baz: v.optional(v.array(v.lazy(() => vFoo))), + qux: v.optional(v.pipe(v.number(), v.integer(), v.gtValue(0)), 0) +}), null); export const vBar = v.object({ foo: v.optional(vFoo) diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-circular-ref-2/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-circular-ref-2/valibot.gen.ts --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-circular-ref-2/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-circular-ref-2/valibot.gen.ts @@ -3,7 +3,7 @@ import * as v from 'valibot'; export const vBar: v.GenericSchema = v.strictObject({ - bar: v.union([v.array(v.lazy(() => vBar)), v.null()]) + bar: v.nullable(v.array(v.lazy(() => vBar))) }); export const vFoo = v.strictObject({ diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-metadata/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-metadata/valibot.gen.ts --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-metadata/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-metadata/valibot.gen.ts @@ -11,12 +11,12 @@ /** * This is Foo schema. */ -export const vFoo: v.GenericSchema = v.optional(v.union([v.object({ - foo: v.optional(v.pipe(v.pipe(v.string(), v.regex(/^\d{3}-\d{2}-\d{4}$/)), v.metadata({ description: 'This is foo property.' }))), - bar: v.optional(v.lazy(() => vBar)), - baz: v.optional(v.pipe(v.array(v.lazy(() => vFoo)), v.metadata({ description: 'This is baz property.' }))), - qux: v.optional(v.pipe(v.pipe(v.number(), v.integer(), v.gtValue(0)), v.metadata({ description: 'This is qux property.' })), 0) - }), v.null()]), null); +export const vFoo: v.GenericSchema = v.nullish(v.object({ + foo: v.optional(v.pipe(v.pipe(v.string(), v.regex(/^\d{3}-\d{2}-\d{4}$/)), v.metadata({ description: 'This is foo property.' }))), + bar: v.optional(v.lazy(() => vBar)), + baz: v.optional(v.pipe(v.array(v.lazy(() => vFoo)), v.metadata({ description: 'This is baz property.' }))), + qux: v.optional(v.pipe(v.pipe(v.number(), v.integer(), v.gtValue(0)), v.metadata({ description: 'This is qux property.' })), 0) +}), null); /** * This is Bar schema. diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-types/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-types/valibot.gen.ts --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-types/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators-types/valibot.gen.ts @@ -11,12 +11,12 @@ /** * This is Foo schema. */ -export const vFoo: v.GenericSchema = v.optional(v.union([v.object({ - foo: v.optional(v.pipe(v.string(), v.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: v.optional(v.lazy(() => vBar)), - baz: v.optional(v.array(v.lazy(() => vFoo))), - qux: v.optional(v.pipe(v.number(), v.integer(), v.gtValue(0)), 0) - }), v.null()]), null); +export const vFoo: v.GenericSchema = v.nullish(v.object({ + foo: v.optional(v.pipe(v.string(), v.regex(/^\d{3}-\d{2}-\d{4}$/))), + bar: v.optional(v.lazy(() => vBar)), + baz: v.optional(v.array(v.lazy(() => vFoo))), + qux: v.optional(v.pipe(v.number(), v.integer(), v.gtValue(0)), 0) +}), null); /** * This is Bar schema. diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators/valibot.gen.ts --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/validators/valibot.gen.ts @@ -11,12 +11,12 @@ /** * This is Foo schema. */ -export const vFoo: v.GenericSchema = v.optional(v.union([v.object({ - foo: v.optional(v.pipe(v.string(), v.regex(/^\d{3}-\d{2}-\d{4}$/))), - bar: v.optional(v.lazy(() => vBar)), - baz: v.optional(v.array(v.lazy(() => vFoo))), - qux: v.optional(v.pipe(v.number(), v.integer(), v.gtValue(0)), 0) - }), v.null()]), null); +export const vFoo: v.GenericSchema = v.nullish(v.object({ + foo: v.optional(v.pipe(v.string(), v.regex(/^\d{3}-\d{2}-\d{4}$/))), + bar: v.optional(v.lazy(() => vBar)), + baz: v.optional(v.array(v.lazy(() => vFoo))), + qux: v.optional(v.pipe(v.number(), v.integer(), v.gtValue(0)), 0) +}), null); /** * This is Bar schema. diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/array.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/array.ts --- a/packages/openapi-ts/src/plugins/valibot/v1/toAst/array.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/array.ts @@ -1,20 +1,19 @@ -import { fromRef, ref } from '@hey-api/codegen-core'; -import type { SchemaWithType } from '@hey-api/shared'; -import { deduplicateSchema } from '@hey-api/shared'; +import type { SchemaResult, SchemaWithType } from '@hey-api/shared'; +import { childContext, deduplicateSchema } from '@hey-api/shared'; import { $ } from '../../../../ts-dsl'; import { pipesToNode } from '../../shared/pipes'; import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; import { identifiers } from '../constants'; -import { irSchemaToAst } from '../plugin'; import { unknownToAst } from './unknown'; export function arrayToAst( options: IrSchemaToAstOptions & { + applyModifiers: (result: SchemaResult, opts: { optional?: boolean }) => Ast; schema: SchemaWithType<'array'>; }, ): Omit { - const { plugin } = options; + const { applyModifiers, plugin, walk } = options; let { schema } = options; const result: Omit = { @@ -39,18 +38,23 @@ // at least one item is guaranteed const itemExpressions = schema.items!.map((item, index) => { - const itemAst = irSchemaToAst({ - ...options, - schema: item, - state: { - ...options.state, - path: ref([...fromRef(options.state.path), 'items', index]), - }, - }); - if (itemAst.hasLazyExpression) { + const itemResult = walk( + item, + childContext( + { + path: options.state.path, + plugin: options.plugin, + }, + 'items', + index, + ), + ); + if (itemResult.hasLazyExpression) { result.hasLazyExpression = true; } - return pipesToNode(itemAst.pipes, plugin); + + const finalExpr = applyModifiers(itemResult, { optional: false }); + return pipesToNode(finalExpr.pipes, plugin); }); if (itemExpressions.length === 1) { diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/boolean.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/boolean.ts --- a/packages/openapi-ts/src/plugins/valibot/v1/toAst/boolean.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/boolean.ts @@ -8,7 +8,7 @@ export function booleanToAst({ plugin, schema, -}: IrSchemaToAstOptions & { +}: Pick & { schema: SchemaWithType<'boolean'>; }): ReturnType { const pipes: Array> = []; diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/enum.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/enum.ts --- a/packages/openapi-ts/src/plugins/valibot/v1/toAst/enum.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/enum.ts @@ -38,15 +38,6 @@ .call($.array(...enumMembers)); } -function nullableNode(ctx: EnumResolverContext): PipeResult | undefined { - const { symbols } = ctx; - const { v } = symbols; - const { isNullable } = ctx.nodes.items(ctx); - if (!isNullable) return; - const currentNode = ctx.pipes.toNode(ctx.pipes.current, ctx.plugin); - return $(v).attr(identifiers.schemas.nullable).call(currentNode); -} - function enumResolver(ctx: EnumResolverContext): PipeResult { const { enumMembers } = ctx.nodes.items(ctx); @@ -57,11 +48,6 @@ const baseExpression = ctx.nodes.base(ctx); ctx.pipes.push(ctx.pipes.current, baseExpression); - const nullableExpression = ctx.nodes.nullable(ctx); - if (nullableExpression) { - return nullableExpression; - } - return ctx.pipes.current; } @@ -69,14 +55,14 @@ plugin, schema, state, -}: IrSchemaToAstOptions & { +}: Pick & { schema: SchemaWithType<'enum'>; }): Pipe { const v = plugin.external('valibot.v'); const { enumMembers } = itemsNode({ $, - nodes: { base: baseNode, items: itemsNode, nullable: nullableNode }, + nodes: { base: baseNode, items: itemsNode }, pipes: { ...pipes, current: [] }, plugin, schema, @@ -90,7 +76,6 @@ schema: { type: 'unknown', }, - state, }); } @@ -99,7 +84,6 @@ nodes: { base: baseNode, items: itemsNode, - nullable: nullableNode, }, pipes: { ...pipes, diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/index.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/index.ts deleted file mode 100644 --- a/packages/openapi-ts/src/plugins/valibot/v1/toAst/index.ts +++ /dev/null @@ -1,130 +0,0 @@ -import type { SchemaWithType } from '@hey-api/shared'; - -import { shouldCoerceToBigInt } from '../../../../plugins/shared/utils/coerce'; -import type { $ } from '../../../../ts-dsl'; -import { pipesToNode } from '../../shared/pipes'; -import type { IrSchemaToAstOptions } from '../../shared/types'; -import { arrayToAst } from './array'; -import { booleanToAst } from './boolean'; -import { enumToAst } from './enum'; -import { neverToAst } from './never'; -import { nullToAst } from './null'; -import { numberToNode } from './number'; -import { objectToAst } from './object'; -import { stringToNode } from './string'; -import { tupleToAst } from './tuple'; -import { undefinedToAst } from './undefined'; -import { unknownToAst } from './unknown'; -import { voidToAst } from './void'; - -export function irSchemaWithTypeToAst({ - schema, - ...args -}: IrSchemaToAstOptions & { - schema: SchemaWithType; -}): { - anyType?: string; - expression: ReturnType; -} { - switch (schema.type) { - case 'array': - return { - expression: pipesToNode( - arrayToAst({ - ...args, - schema: schema as SchemaWithType<'array'>, - }).pipes, - args.plugin, - ), - }; - case 'boolean': - return { - expression: booleanToAst({ - ...args, - schema: schema as SchemaWithType<'boolean'>, - }), - }; - case 'enum': - return { - expression: enumToAst({ - ...args, - schema: schema as SchemaWithType<'enum'>, - }), - }; - case 'integer': - case 'number': - return { - expression: numberToNode({ - ...args, - schema: schema as SchemaWithType<'integer' | 'number'>, - }), - }; - case 'never': - return { - expression: neverToAst({ - ...args, - schema: schema as SchemaWithType<'never'>, - }), - }; - case 'null': - return { - expression: nullToAst({ - ...args, - schema: schema as SchemaWithType<'null'>, - }), - }; - case 'object': - return { - expression: pipesToNode( - objectToAst({ - ...args, - schema: schema as SchemaWithType<'object'>, - }).pipes, - args.plugin, - ), - }; - case 'string': - return { - expression: shouldCoerceToBigInt(schema.format) - ? numberToNode({ - ...args, - schema: { ...schema, type: 'number' }, - }) - : stringToNode({ - ...args, - schema: schema as SchemaWithType<'string'>, - }), - }; - case 'tuple': - return { - expression: pipesToNode( - tupleToAst({ - ...args, - schema: schema as SchemaWithType<'tuple'>, - }).pipes, - args.plugin, - ), - }; - case 'undefined': - return { - expression: undefinedToAst({ - ...args, - schema: schema as SchemaWithType<'undefined'>, - }), - }; - case 'unknown': - return { - expression: unknownToAst({ - ...args, - schema: schema as SchemaWithType<'unknown'>, - }), - }; - case 'void': - return { - expression: voidToAst({ - ...args, - schema: schema as SchemaWithType<'void'>, - }), - }; - } -} diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/never.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/never.ts --- a/packages/openapi-ts/src/plugins/valibot/v1/toAst/never.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/never.ts @@ -6,7 +6,7 @@ export function neverToAst({ plugin, -}: IrSchemaToAstOptions & { +}: Pick & { schema: SchemaWithType<'never'>; }) { const v = plugin.external('valibot.v'); diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/null.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/null.ts --- a/packages/openapi-ts/src/plugins/valibot/v1/toAst/null.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/null.ts @@ -6,7 +6,7 @@ export function nullToAst({ plugin, -}: IrSchemaToAstOptions & { +}: Pick & { schema: SchemaWithType<'null'>; }) { const v = plugin.external('valibot.v'); diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/number.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/number.ts --- a/packages/openapi-ts/src/plugins/valibot/v1/toAst/number.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/number.ts @@ -108,7 +108,7 @@ export function numberToNode({ plugin, schema, -}: IrSchemaToAstOptions & { +}: Pick & { schema: SchemaWithType<'integer' | 'number'>; }): Pipe { const ctx: NumberResolverContext = { diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/object.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/object.ts --- a/packages/openapi-ts/src/plugins/valibot/v1/toAst/object.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/object.ts @@ -1,33 +1,44 @@ -import { fromRef, ref } from '@hey-api/codegen-core'; -import type { SchemaWithType } from '@hey-api/shared'; +import type { SchemaResult, SchemaVisitorContext, SchemaWithType, Walker } from '@hey-api/shared'; +import { childContext } from '@hey-api/shared'; import { $ } from '../../../../ts-dsl'; import type { ObjectResolverContext } from '../../resolvers'; import type { Pipe, PipeResult } from '../../shared/pipes'; import { pipes } from '../../shared/pipes'; import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; +import type { ValibotPlugin } from '../../types'; import { identifiers } from '../constants'; -import { irSchemaToAst } from '../plugin'; -function additionalPropertiesNode(ctx: ObjectResolverContext): Pipe | null | undefined { - const { plugin, schema } = ctx; +type WalkerCtx = SchemaVisitorContext; + +interface ObjectToAstOptions extends IrSchemaToAstOptions { + applyModifiers: (result: SchemaResult, opts: { optional?: boolean }) => Ast; + schema: SchemaWithType<'object'>; + walk: Walker; + walkerCtx: WalkerCtx; +} + +type ExtendedContext = ObjectResolverContext & { + applyModifiers: ObjectToAstOptions['applyModifiers']; + walk: ObjectToAstOptions['walk']; + walkerCtx: ObjectToAstOptions['walkerCtx']; +}; + +function additionalPropertiesNode(ctx: ExtendedContext): Pipe | null | undefined { + const { plugin, schema, walk, walkerCtx } = ctx; if (!schema.additionalProperties || !schema.additionalProperties.type) return; if (schema.additionalProperties.type === 'never') return null; - const additionalAst = irSchemaToAst({ - ...ctx, - schema: schema.additionalProperties, - state: { - ...ctx.utils.state, - path: ref([...fromRef(ctx.utils.state.path), 'additionalProperties']), - }, - }); - if (additionalAst.hasLazyExpression) ctx.utils.ast.hasLazyExpression = true; - return pipes.toNode(additionalAst.pipes, plugin); + const additionalResult = walk( + schema.additionalProperties, + childContext(walkerCtx, 'additionalProperties'), + ); + if (additionalResult.hasLazyExpression) ctx.utils.ast.hasLazyExpression = true; + return pipes.toNode(additionalResult.expression.pipes, plugin); } -function baseNode(ctx: ObjectResolverContext): PipeResult { +function baseNode(ctx: ExtendedContext): PipeResult { const { nodes, symbols } = ctx; const { v } = symbols; @@ -51,43 +62,38 @@ return $(v).attr(identifiers.schemas.object).call(shape); } -function objectResolver(ctx: ObjectResolverContext): PipeResult { +function objectResolver(ctx: ExtendedContext): PipeResult { // TODO: parser - handle constants return ctx.nodes.base(ctx); } -function shapeNode(ctx: ObjectResolverContext): ReturnType { - const { plugin, schema } = ctx; +function shapeNode(ctx: ExtendedContext): ReturnType { + const { applyModifiers, plugin, schema, walk, walkerCtx } = ctx; const shape = $.object().pretty(); for (const name in schema.properties) { const property = schema.properties[name]!; + const isOptional = !schema.required?.includes(name); - const propertyAst = irSchemaToAst({ - ...ctx, - optional: !schema.required?.includes(name), - schema: property, - state: { - ...ctx.utils.state, - path: ref([...fromRef(ctx.utils.state.path), 'properties', name]), - }, + const propertyResult = walk(property, childContext(walkerCtx, 'properties', name)); + if (propertyResult.hasLazyExpression) ctx.utils.ast.hasLazyExpression = true; + + const ast = applyModifiers(propertyResult, { + optional: isOptional, }); - if (propertyAst.hasLazyExpression) ctx.utils.ast.hasLazyExpression = true; - shape.prop(name, pipes.toNode(propertyAst.pipes, plugin)); + + shape.prop(name, pipes.toNode(ast.pipes, plugin)); } return shape; } -export function objectToAst( - options: IrSchemaToAstOptions & { - schema: SchemaWithType<'object'>; - }, -): Omit { - const { plugin } = options; - const ctx: ObjectResolverContext = { +export function objectToAst(options: ObjectToAstOptions): Omit { + const { applyModifiers, plugin, walk, walkerCtx } = options; + const ctx: ExtendedContext = { ...options, $, + applyModifiers, nodes: { additionalProperties: additionalPropertiesNode, base: baseNode, @@ -104,6 +110,8 @@ ast: {}, state: options.state, }, + walk, + walkerCtx, }; const resolver = plugin.config['~resolvers']?.object; const node = resolver?.(ctx) ?? objectResolver(ctx); diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/string.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/string.ts --- a/packages/openapi-ts/src/plugins/valibot/v1/toAst/string.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/string.ts @@ -101,7 +101,7 @@ export function stringToNode({ plugin, schema, -}: IrSchemaToAstOptions & { +}: Pick & { schema: SchemaWithType<'string'>; }): Pipe { const ctx: StringResolverContext = { diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/tuple.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/tuple.ts --- a/packages/openapi-ts/src/plugins/valibot/v1/toAst/tuple.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/tuple.ts @@ -1,19 +1,19 @@ -import { fromRef, ref } from '@hey-api/codegen-core'; -import type { SchemaWithType } from '@hey-api/shared'; +import type { SchemaResult, SchemaWithType } from '@hey-api/shared'; +import { childContext } from '@hey-api/shared'; import { $ } from '../../../../ts-dsl'; import { pipesToNode } from '../../shared/pipes'; import type { Ast, IrSchemaToAstOptions } from '../../shared/types'; import { identifiers } from '../constants'; -import { irSchemaToAst } from '../plugin'; import { unknownToAst } from './unknown'; export function tupleToAst( options: IrSchemaToAstOptions & { + applyModifiers: (result: SchemaResult, opts: { optional?: boolean }) => Ast; schema: SchemaWithType<'tuple'>; }, ): Omit { - const { plugin, schema } = options; + const { applyModifiers, plugin, schema, walk } = options; const result: Partial> = {}; @@ -33,18 +33,23 @@ if (schema.items) { const tupleElements = schema.items.map((item, index) => { - const schemaPipes = irSchemaToAst({ - ...options, - schema: item, - state: { - ...options.state, - path: ref([...fromRef(options.state.path), 'items', index]), - }, - }); - if (schemaPipes.hasLazyExpression) { + const itemResult = walk( + item, + childContext( + { + path: options.state.path, + plugin: options.plugin, + }, + 'items', + index, + ), + ); + if (itemResult.hasLazyExpression) { result.hasLazyExpression = true; } - return pipesToNode(schemaPipes.pipes, plugin); + + const finalExpr = applyModifiers(itemResult, { optional: false }); + return pipesToNode(finalExpr.pipes, plugin); }); result.pipes = [ $(v) diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/undefined.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/undefined.ts --- a/packages/openapi-ts/src/plugins/valibot/v1/toAst/undefined.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/undefined.ts @@ -6,7 +6,7 @@ export function undefinedToAst({ plugin, -}: IrSchemaToAstOptions & { +}: Pick & { schema: SchemaWithType<'undefined'>; }) { const v = plugin.external('valibot.v'); diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/unknown.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/unknown.ts --- a/packages/openapi-ts/src/plugins/valibot/v1/toAst/unknown.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/unknown.ts @@ -6,7 +6,7 @@ export function unknownToAst({ plugin, -}: IrSchemaToAstOptions & { +}: Pick & { schema: SchemaWithType<'unknown'>; }) { const v = plugin.external('valibot.v'); diff --git a/packages/openapi-ts/src/plugins/valibot/v1/toAst/void.ts b/packages/openapi-ts/src/plugins/valibot/v1/toAst/void.ts --- a/packages/openapi-ts/src/plugins/valibot/v1/toAst/void.ts +++ b/packages/openapi-ts/src/plugins/valibot/v1/toAst/void.ts @@ -6,7 +6,7 @@ export function voidToAst({ plugin, -}: IrSchemaToAstOptions & { +}: Pick & { schema: SchemaWithType<'void'>; }) { const v = plugin.external('valibot.v'); diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/valibot/default/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/valibot/default/valibot.gen.ts --- a/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/valibot/default/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/2.0.x/plugins/valibot/default/valibot.gen.ts @@ -197,8 +197,8 @@ * This is a model with one string property */ export const vModelWithNullableString = v.object({ - nullableProp: v.optional(v.union([v.string(), v.null()])), - nullableRequiredProp: v.union([v.string(), v.null()]) + nullableProp: v.nullish(v.string()), + nullableRequiredProp: v.nullable(v.string()) }); /** @@ -581,8 +581,8 @@ parameterStringWithDefault: v.optional(v.string(), 'Hello World!'), parameterStringWithEmptyDefault: v.optional(v.string(), ''), parameterStringWithNoDefault: v.string(), - parameterStringNullableWithNoDefault: v.optional(v.union([v.string(), v.null()])), - parameterStringNullableWithDefault: v.optional(v.union([v.string(), v.null()]), null) + parameterStringNullableWithNoDefault: v.nullish(v.string()), + parameterStringNullableWithDefault: v.nullish(v.string(), null) }) }); diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/valibot/default/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/valibot/default/valibot.gen.ts --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/valibot/default/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.0.x/plugins/valibot/default/valibot.gen.ts @@ -81,7 +81,7 @@ /** * This is a simple string */ -export const vSimpleStringWithPattern = v.union([v.pipe(v.string(), v.maxLength(64), v.regex(/^[a-zA-Z0-9_]*$/)), v.null()]); +export const vSimpleStringWithPattern = v.nullable(v.pipe(v.string(), v.maxLength(64), v.regex(/^[a-zA-Z0-9_]*$/))); /** * This is a simple enum with strings @@ -243,10 +243,10 @@ * This is a model with one string property */ export const vModelWithNullableString = v.object({ - nullableProp1: v.optional(v.union([v.string(), v.null()])), - nullableRequiredProp1: v.union([v.string(), v.null()]), - nullableProp2: v.optional(v.union([v.string(), v.null()])), - nullableRequiredProp2: v.union([v.string(), v.null()]), + nullableProp1: v.nullish(v.string()), + nullableRequiredProp1: v.nullable(v.string()), + nullableProp2: v.nullish(v.string()), + nullableRequiredProp2: v.nullable(v.string()), 'foo_bar-enum': v.optional(v.picklist([ 'Success', 'Warning', @@ -425,7 +425,7 @@ * This is a model with nested 'any of' property with a type null */ export const vCompositionWithNestedAnyAndTypeNull = v.object({ - propA: v.optional(v.union([v.array(v.union([vModelWithDictionary, v.null()])), v.array(v.union([vModelWithArray, v.null()]))])) + propA: v.optional(v.union([v.array(v.nullable(vModelWithDictionary)), v.array(v.nullable(vModelWithArray))])) }); export const v3eNum1Период = v.picklist(['Bird', 'Dog']); @@ -436,21 +436,20 @@ * This is a model with one property with a 'any of' relationship where the options are not $ref */ export const vCompositionWithNestedAnyOfAndNull = v.object({ - propA: v.optional(v.union([v.array(v.unknown()), v.null()])) + propA: v.nullish(v.array(v.unknown())) }); /** * This is a model with one property with a 'one of' relationship */ export const vCompositionWithOneOfAndNullable = v.object({ - propA: v.optional(v.union([ + propA: v.nullish(v.union([ v.object({ boolean: v.optional(v.boolean()) }), vModelWithEnum, vModelWithArray, - vModelWithDictionary, - v.null() + vModelWithDictionary ])) }); @@ -479,28 +478,27 @@ * This is a model with one property with a 'all of' relationship */ export const vCompositionWithAllOfAndNullable = v.object({ - propA: v.optional(v.union([v.intersect([ - v.object({ - boolean: v.optional(v.boolean()) - }), - vModelWithEnum, - vModelWithArray, - vModelWithDictionary - ]), v.null()])) + propA: v.nullish(v.intersect([ + v.object({ + boolean: v.optional(v.boolean()) + }), + vModelWithEnum, + vModelWithArray, + vModelWithDictionary + ])) }); /** * This is a model with one property with a 'any of' relationship */ export const vCompositionWithAnyOfAndNullable = v.object({ - propA: v.optional(v.union([ + propA: v.nullish(v.union([ v.object({ boolean: v.optional(v.boolean()) }), vModelWithEnum, vModelWithArray, - vModelWithDictionary, - v.null() + vModelWithDictionary ])) }); @@ -527,7 +525,7 @@ export const vModelWithProperties = v.object({ required: v.string(), requiredAndReadOnly: v.pipe(v.string(), v.readonly()), - requiredAndNullable: v.union([v.string(), v.null()]), + requiredAndNullable: v.nullable(v.string()), string: v.optional(v.string()), number: v.optional(v.number()), boolean: v.optional(v.boolean()), @@ -550,11 +548,11 @@ * This is a model with one nested property */ export const vModelWithNestedProperties = v.object({ - first: v.pipe(v.union([v.pipe(v.object({ - second: v.pipe(v.union([v.pipe(v.object({ - third: v.pipe(v.union([v.pipe(v.string(), v.readonly()), v.null()]), v.readonly()) - }), v.readonly()), v.null()]), v.readonly()) - }), v.readonly()), v.null()]), v.readonly()) + first: v.nullable(v.pipe(v.object({ + second: v.nullable(v.pipe(v.object({ + third: v.nullable(v.pipe(v.string(), v.readonly())) + }), v.readonly())) + }), v.readonly())) }); /** @@ -665,15 +663,15 @@ }, v.unknown()); export const vNestedAnyOfArraysNullable = v.object({ - nullableArray: v.optional(v.union([v.array(v.unknown()), v.null()])) + nullableArray: v.nullish(v.array(v.unknown())) }); /** * An object that can be null */ -export const vNullableObject = v.optional(v.union([v.object({ - foo: v.optional(v.string()) - }), v.null()]), null); +export const vNullableObject = v.nullish(v.object({ + foo: v.optional(v.string()) +}), null); /** * Some % character @@ -747,21 +745,9 @@ export const vModelWithPrefixItemsConstantSizeArray = v.array(v.unknown()); export const vModelWithAnyOfConstantSizeArrayNullable = v.tuple([ - v.union([ - v.number(), - v.null(), - v.string() - ]), - v.union([ - v.number(), - v.null(), - v.string() - ]), - v.union([ - v.number(), - v.null(), - v.string() - ]) + v.nullable(v.union([v.number(), v.string()])), + v.nullable(v.union([v.number(), v.string()])), + v.nullable(v.union([v.number(), v.string()])) ]); export const vModelWithAnyOfConstantSizeArrayAndIntersect = v.tuple([v.intersect([v.number(), v.string()]), v.intersect([v.number(), v.string()])]); @@ -860,14 +846,14 @@ export const vGenericSchemaDuplicateIssue1SystemBoolean = v.strictObject({ item: v.optional(v.boolean()), - error: v.optional(v.union([v.string(), v.null()])), + error: v.nullish(v.string()), hasError: v.optional(v.pipe(v.boolean(), v.readonly())), data: v.optional(v.strictObject({})) }); export const vGenericSchemaDuplicateIssue1SystemString = v.strictObject({ - item: v.optional(v.union([v.string(), v.null()])), - error: v.optional(v.union([v.string(), v.null()])), + item: v.nullish(v.string()), + error: v.nullish(v.string()), hasError: v.optional(v.pipe(v.boolean(), v.readonly())) }); @@ -887,7 +873,7 @@ */ export const vModelWithPropertiesWritable = v.object({ required: v.string(), - requiredAndNullable: v.union([v.string(), v.null()]), + requiredAndNullable: v.nullable(v.string()), string: v.optional(v.string()), number: v.optional(v.number()), boolean: v.optional(v.boolean()), @@ -943,13 +929,13 @@ export const vGenericSchemaDuplicateIssue1SystemBooleanWritable = v.strictObject({ item: v.optional(v.boolean()), - error: v.optional(v.union([v.string(), v.null()])), + error: v.nullish(v.string()), data: v.optional(v.strictObject({})) }); export const vGenericSchemaDuplicateIssue1SystemStringWritable = v.strictObject({ - item: v.optional(v.union([v.string(), v.null()])), - error: v.optional(v.union([v.string(), v.null()])) + item: v.nullish(v.string()), + error: v.nullish(v.string()) }); /** @@ -962,12 +948,12 @@ }), v.strictObject({ bar: vNonAsciiStringæøåÆøÅöôêÊ字符串 })]), v.object({ - baz: v.union([v.pipe(v.number(), v.integer(), v.minValue(0), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 65535')), v.null()]), + baz: v.nullable(v.pipe(v.number(), v.integer(), v.minValue(0), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 65535'))), qux: v.pipe(v.number(), v.integer(), v.minValue(0), v.maxValue(255, 'Invalid value: Expected uint8 to be <= 255')) })]); export const vModelWithOneOfAndProperties = v.intersect([v.union([vSimpleParameter, vNonAsciiStringæøåÆøÅöôêÊ字符串]), v.object({ - baz: v.union([v.pipe(v.number(), v.integer(), v.minValue(0), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 65535')), v.null()]), + baz: v.nullable(v.pipe(v.number(), v.integer(), v.minValue(0), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 65535'))), qux: v.pipe(v.number(), v.integer(), v.minValue(0), v.maxValue(255, 'Invalid value: Expected uint8 to be <= 255')) })]); @@ -1102,40 +1088,40 @@ path: v.optional(v.never()), query: v.optional(v.never()), headers: v.object({ - parameter: v.union([vDeprecatedModel, v.null()]) + parameter: v.nullable(vDeprecatedModel) }) }); export const vCallWithParametersData = v.object({ - body: v.union([v.record(v.string(), v.unknown()), v.null()]), + body: v.nullable(v.record(v.string(), v.unknown())), path: v.object({ - parameterPath: v.union([v.string(), v.null()]), - 'api-version': v.union([v.string(), v.null()]) + parameterPath: v.nullable(v.string()), + 'api-version': v.nullable(v.string()) }), query: v.object({ foo_ref_enum: v.optional(vModelWithNestedArrayEnumsDataFoo), foo_all_of_enum: vModelWithNestedArrayEnumsDataFoo, - cursor: v.union([v.string(), v.null()]) + cursor: v.nullable(v.string()) }), headers: v.object({ - parameterHeader: v.union([v.string(), v.null()]) + parameterHeader: v.nullable(v.string()) }) }); export const vCallWithWeirdParameterNamesData = v.object({ - body: v.union([vModelWithString, v.null()]), + body: v.nullable(vModelWithString), path: v.object({ 'parameter.path.1': v.optional(v.string()), 'parameter-path-2': v.optional(v.string()), 'PARAMETER-PATH-3': v.optional(v.string()), - 'api-version': v.union([v.string(), v.null()]) + 'api-version': v.nullable(v.string()) }), query: v.object({ default: v.optional(v.string()), - 'parameter-query': v.union([v.string(), v.null()]) + 'parameter-query': v.nullable(v.string()) }), headers: v.object({ - 'parameter.header': v.union([v.string(), v.null()]) + 'parameter.header': v.nullable(v.string()) }) }); @@ -1149,7 +1135,7 @@ export const vPostCallWithOptionalParamData = v.object({ body: v.optional(v.object({ - offset: v.optional(v.union([v.number(), v.null()])) + offset: v.nullish(v.number()) })), path: v.optional(v.never()), query: v.object({ @@ -1179,15 +1165,15 @@ body: v.optional(v.never()), path: v.optional(v.never()), query: v.optional(v.object({ - parameterString: v.optional(v.union([v.optional(v.string(), 'Hello World!'), v.null()]), 'Hello World!'), - parameterNumber: v.optional(v.union([v.optional(v.number(), 123), v.null()]), 123), - parameterBoolean: v.optional(v.union([v.optional(v.boolean(), true), v.null()]), true), + parameterString: v.nullish(v.string(), 'Hello World!'), + parameterNumber: v.nullish(v.number(), 123), + parameterBoolean: v.nullish(v.boolean(), true), parameterEnum: v.optional(v.picklist([ 'Success', 'Warning', 'Error' ])), - parameterModel: v.optional(v.union([vModelWithString, v.null()])) + parameterModel: v.nullish(vModelWithString) })) }); @@ -1217,8 +1203,8 @@ parameterStringWithDefault: v.optional(v.string(), 'Hello World!'), parameterStringWithEmptyDefault: v.optional(v.string(), ''), parameterStringWithNoDefault: v.string(), - parameterStringNullableWithNoDefault: v.optional(v.union([v.string(), v.null()])), - parameterStringNullableWithDefault: v.optional(v.union([v.string(), v.null()]), null) + parameterStringNullableWithNoDefault: v.nullish(v.string()), + parameterStringNullableWithDefault: v.nullish(v.string(), null) }) }); @@ -1320,11 +1306,11 @@ body: v.optional(v.never()), path: v.optional(v.never()), query: v.object({ - parameterArrayCSV: v.union([v.array(v.string()), v.null()]), - parameterArraySSV: v.union([v.array(v.string()), v.null()]), - parameterArrayTSV: v.union([v.array(v.string()), v.null()]), - parameterArrayPipes: v.union([v.array(v.string()), v.null()]), - parameterArrayMulti: v.union([v.array(v.string()), v.null()]) + parameterArrayCSV: v.nullable(v.array(v.string())), + parameterArraySSV: v.nullable(v.array(v.string())), + parameterArrayTSV: v.nullable(v.array(v.string())), + parameterArrayPipes: v.nullable(v.array(v.string())), + parameterArrayMulti: v.nullable(v.array(v.string())) }) }); @@ -1335,11 +1321,11 @@ })), query: v.object({ parameterNumber: v.optional(v.number(), 123), - parameterString: v.optional(v.union([v.optional(v.string(), 'default'), v.null()]), 'default'), - parameterBoolean: v.optional(v.union([v.optional(v.boolean(), true), v.null()]), true), - parameterObject: v.optional(v.union([v.record(v.string(), v.unknown()), v.null()]), null), - parameterArray: v.union([v.array(v.string()), v.null()]), - parameterDictionary: v.union([v.record(v.string(), v.unknown()), v.null()]), + parameterString: v.nullish(v.string(), 'default'), + parameterBoolean: v.nullish(v.boolean(), true), + parameterObject: v.nullish(v.record(v.string(), v.unknown()), null), + parameterArray: v.nullable(v.array(v.string())), + parameterDictionary: v.nullable(v.record(v.string(), v.unknown())), parameterEnum: v.picklist([ 'Success', 'Warning', @@ -1358,7 +1344,7 @@ export const vUploadFileData = v.object({ body: v.string(), path: v.object({ - 'api-version': v.union([v.string(), v.null()]) + 'api-version': v.nullable(v.string()) }), query: v.optional(v.never()) }); @@ -1419,7 +1405,7 @@ export const vMultipartRequestData = v.object({ body: v.optional(v.object({ content: v.optional(v.string()), - data: v.optional(v.union([vModelWithString, v.null()])) + data: v.nullish(vModelWithString) })), path: v.optional(v.never()), query: v.optional(v.never()) @@ -1427,16 +1413,16 @@ export const vComplexParamsData = v.object({ body: v.optional(v.object({ - key: v.pipe(v.union([v.pipe(v.pipe(v.string(), v.maxLength(64), v.regex(/^[a-zA-Z0-9_]*$/)), v.readonly()), v.null()]), v.readonly()), - name: v.union([v.pipe(v.string(), v.maxLength(255)), v.null()]), + key: v.nullable(v.pipe(v.pipe(v.string(), v.maxLength(64), v.regex(/^[a-zA-Z0-9_]*$/)), v.readonly())), + name: v.nullable(v.pipe(v.string(), v.maxLength(255))), enabled: v.optional(v.boolean(), true), type: v.picklist([ 'Monkey', 'Horse', 'Bird' ]), - listOfModels: v.optional(v.union([v.array(vModelWithString), v.null()])), - listOfStrings: v.optional(v.union([v.array(v.string()), v.null()])), + listOfModels: v.nullish(v.array(vModelWithString)), + listOfStrings: v.nullish(v.array(v.string())), parameters: v.union([ vModelWithString, vModelWithEnum, @@ -1445,7 +1431,7 @@ ]), user: v.optional(v.pipe(v.object({ id: v.optional(v.pipe(v.pipe(v.number(), v.integer(), v.minValue(-2147483648, 'Invalid value: Expected int32 to be >= -2147483648'), v.maxValue(2147483647, 'Invalid value: Expected int32 to be <= 2147483647')), v.readonly())), - name: v.optional(v.pipe(v.union([v.pipe(v.string(), v.readonly()), v.null()]), v.readonly())) + name: v.nullish(v.pipe(v.string(), v.readonly())) }), v.readonly())) })), path: v.object({ diff --git a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/valibot/default/valibot.gen.ts b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/valibot/default/valibot.gen.ts --- a/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/valibot/default/valibot.gen.ts +++ b/packages/openapi-ts-tests/main/test/__snapshots__/3.1.x/plugins/valibot/default/valibot.gen.ts @@ -81,7 +81,7 @@ /** * This is a simple string */ -export const vSimpleStringWithPattern = v.union([v.pipe(v.string(), v.maxLength(64), v.regex(/^[a-zA-Z0-9_]*$/)), v.null()]); +export const vSimpleStringWithPattern = v.nullable(v.pipe(v.string(), v.maxLength(64), v.regex(/^[a-zA-Z0-9_]*$/))); /** * This is a simple enum with strings @@ -148,7 +148,7 @@ export const vArrayWithAnyOfProperties = v.array(v.unknown()); export const vAnyOfAnyAndNull = v.object({ - data: v.optional(v.union([v.unknown(), v.null()])) + data: v.nullish(v.unknown()) }); /** @@ -243,10 +243,10 @@ * This is a model with one string property */ export const vModelWithNullableString = v.object({ - nullableProp1: v.optional(v.union([v.string(), v.null()])), - nullableRequiredProp1: v.union([v.string(), v.null()]), - nullableProp2: v.optional(v.union([v.string(), v.null()])), - nullableRequiredProp2: v.union([v.string(), v.null()]), + nullableProp1: v.nullish(v.string()), + nullableRequiredProp1: v.nullable(v.string()), + nullableProp2: v.nullish(v.string()), + nullableRequiredProp2: v.nullable(v.string()), 'foo_bar-enum': v.optional(v.picklist([ 'Success', 'Warning', @@ -436,21 +436,20 @@ * This is a model with one property with a 'any of' relationship where the options are not $ref */ export const vCompositionWithNestedAnyOfAndNull = v.object({ - propA: v.optional(v.union([v.array(v.unknown()), v.null()])) + propA: v.nullish(v.array(v.unknown())) }); /** * This is a model with one property with a 'one of' relationship */ export const vCompositionWithOneOfAndNullable = v.object({ - propA: v.optional(v.union([ + propA: v.nullish(v.union([ v.object({ boolean: v.optional(v.boolean()) }), vModelWithEnum, vModelWithArray, - vModelWithDictionary, - v.null() + vModelWithDictionary ])) }); @@ -479,28 +478,27 @@ * This is a model with one property with a 'all of' relationship */ export const vCompositionWithAllOfAndNullable = v.object({ - propA: v.optional(v.union([v.intersect([ - v.object({ - boolean: v.optional(v.boolean()) - }), - vModelWithEnum, - vModelWithArray, - vModelWithDictionary - ]), v.null()])) + propA: v.nullish(v.intersect([ + v.object({ + boolean: v.optional(v.boolean()) + }), + vModelWithEnum, + vModelWithArray, + vModelWithDictionary + ])) }); /** * This is a model with one property with a 'any of' relationship */ export const vCompositionWithAnyOfAndNullable = v.object({ - propA: v.optional(v.union([ + propA: v.nullish(v.union([ v.object({ boolean: v.optional(v.boolean()) }), vModelWithEnum, vModelWithArray, - vModelWithDictionary, - v.null() + vModelWithDictionary ])) }); @@ -527,7 +525,7 @@ export const vModelWithProperties = v.object({ required: v.string(), requiredAndReadOnly: v.pipe(v.string(), v.readonly()), - requiredAndNullable: v.union([v.string(), v.null()]), + requiredAndNullable: v.nullable(v.string()), string: v.optional(v.string()), number: v.optional(v.number()), boolean: v.optional(v.boolean()), @@ -550,11 +548,11 @@ * This is a model with one nested property */ export const vModelWithNestedProperties = v.object({ - first: v.pipe(v.union([v.pipe(v.object({ - second: v.pipe(v.union([v.pipe(v.object({ - third: v.pipe(v.union([v.pipe(v.string(), v.readonly()), v.null()]), v.readonly()) - }), v.readonly()), v.null()]), v.readonly()) - }), v.readonly()), v.null()]), v.readonly()) + first: v.nullable(v.pipe(v.object({ + second: v.nullable(v.pipe(v.object({ + third: v.nullable(v.pipe(v.string(), v.readonly())) + }), v.readonly())) + }), v.readonly())) }); /** @@ -665,15 +663,15 @@ }, v.unknown()); export const vNestedAnyOfArraysNullable = v.object({ - nullableArray: v.optional(v.union([v.array(v.unknown()), v.null()])) + nullableArray: v.nullish(v.array(v.unknown())) }); /** * An object that can be null */ -export const vNullableObject = v.optional(v.union([v.object({ - foo: v.optional(v.string()) - }), v.null()]), null); +export const vNullableObject = v.nullish(v.object({ + foo: v.optional(v.string()) +}), null); /** * Some % character @@ -751,21 +749,9 @@ ]); export const vModelWithAnyOfConstantSizeArrayNullable = v.tuple([ - v.union([ - v.number(), - v.null(), - v.string() - ]), - v.union([ - v.number(), - v.null(), - v.string() - ]), - v.union([ - v.number(), - v.null(), - v.string() - ]) + v.nullable(v.union([v.number(), v.string()])), + v.nullable(v.union([v.number(), v.string()])), + v.nullable(v.union([v.number(), v.string()])) ]); export const vModelWithAnyOfConstantSizeArrayAndIntersect = v.tuple([v.intersect([v.number(), v.string()]), v.intersect([v.number(), v.string()])]); @@ -864,14 +850,14 @@ export const vGenericSchemaDuplicateIssue1SystemBoolean = v.strictObject({ item: v.optional(v.boolean()), - error: v.optional(v.union([v.string(), v.null()])), + error: v.nullish(v.string()), hasError: v.optional(v.pipe(v.boolean(), v.readonly())), data: v.optional(v.strictObject({})) }); export const vGenericSchemaDuplicateIssue1SystemString = v.strictObject({ - item: v.optional(v.union([v.string(), v.null()])), - error: v.optional(v.union([v.string(), v.null()])), + item: v.nullish(v.string()), + error: v.nullish(v.string()), hasError: v.optional(v.pipe(v.boolean(), v.readonly())) }); @@ -897,7 +883,7 @@ */ export const vModelWithPropertiesWritable = v.object({ required: v.string(), - requiredAndNullable: v.union([v.string(), v.null()]), + requiredAndNullable: v.nullable(v.string()), string: v.optional(v.string()), number: v.optional(v.number()), boolean: v.optional(v.boolean()), @@ -953,13 +939,13 @@ export const vGenericSchemaDuplicateIssue1SystemBooleanWritable = v.strictObject({ item: v.optional(v.boolean()), - error: v.optional(v.union([v.string(), v.null()])), + error: v.nullish(v.string()), data: v.optional(v.strictObject({})) }); export const vGenericSchemaDuplicateIssue1SystemStringWritable = v.strictObject({ - item: v.optional(v.union([v.string(), v.null()])), - error: v.optional(v.union([v.string(), v.null()])) + item: v.nullish(v.string()), + error: v.nullish(v.string()) }); /** @@ -972,12 +958,12 @@ }), v.strictObject({ bar: vNonAsciiStringæøåÆøÅöôêÊ字符串 })]), v.object({ - baz: v.union([v.pipe(v.number(), v.integer(), v.minValue(0), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 65535')), v.null()]), + baz: v.nullable(v.pipe(v.number(), v.integer(), v.minValue(0), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 65535'))), qux: v.pipe(v.number(), v.integer(), v.minValue(0), v.maxValue(255, 'Invalid value: Expected uint8 to be <= 255')) })]); export const vModelWithOneOfAndProperties = v.intersect([v.union([vSimpleParameter, vNonAsciiStringæøåÆøÅöôêÊ字符串]), v.object({ - baz: v.union([v.pipe(v.number(), v.integer(), v.minValue(0), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 65535')), v.null()]), + baz: v.nullable(v.pipe(v.number(), v.integer(), v.minValue(0), v.maxValue(65535, 'Invalid value: Expected uint16 to be <= 65535'))), qux: v.pipe(v.number(), v.integer(), v.minValue(0), v.maxValue(255, 'Invalid value: Expected uint8 to be <= 255')) })]); @@ -1118,40 +1104,40 @@ path: v.optional(v.never()), query: v.optional(v.never()), headers: v.object({ - parameter: v.union([vDeprecatedModel, v.null()]) + parameter: v.nullable(vDeprecatedModel) }) }); export const vCallWithParametersData = v.object({ - body: v.union([v.record(v.string(), v.unknown()), v.null()]), + body: v.nullable(v.record(v.string(), v.unknown())), path: v.object({ - parameterPath: v.union([v.string(), v.null()]), - 'api-version': v.union([v.string(), v.null()]) + parameterPath: v.nullable(v.string()), + 'api-version': v.nullable(v.string()) }), query: v.object({ foo_ref_enum: v.optional(vModelWithNestedArrayEnumsDataFoo), foo_all_of_enum: vModelWithNestedArrayEnumsDataFoo, - cursor: v.union([v.string(), v.null()]) + cursor: v.nullable(v.string()) }), headers: v.object({ - parameterHeader: v.union([v.string(), v.null()]) + parameterHeader: v.nullable(v.string()) }) }); export const vCallWithWeirdParameterNamesData = v.object({ - body: v.union([vModelWithString, v.null()]), + body: v.nullable(vModelWithString), path: v.object({ 'parameter.path.1': v.optional(v.string()), 'parameter-path-2': v.optional(v.string()), 'PARAMETER-PATH-3': v.optional(v.string()), - 'api-version': v.union([v.string(), v.null()]) + 'api-version': v.nullable(v.string()) }), query: v.object({ default: v.optional(v.string()), - 'parameter-query': v.union([v.string(), v.null()]) + 'parameter-query': v.nullable(v.string()) }), headers: v.object({ - 'parameter.header': v.union([v.string(), v.null()]) + 'parameter.header': v.nullable(v.string()) }) }); @@ -1165,7 +1151,7 @@ export const vPostCallWithOptionalParamData = v.object({ body: v.optional(v.object({ - offset: v.optional(v.union([v.number(), v.null()])) + offset: v.nullish(v.number()) })), path: v.optional(v.never()), query: v.object({ @@ -1195,15 +1181,15 @@ body: v.optional(v.never()), path: v.optional(v.never()), query: v.optional(v.object({ - parameterString: v.optional(v.union([v.optional(v.string(), 'Hello World!'), v.null()]), 'Hello World!'), - parameterNumber: v.optional(v.union([v.optional(v.number(), 123), v.null()]), 123), - parameterBoolean: v.optional(v.union([v.optional(v.boolean(), true), v.null()]), true), + parameterString: v.nullish(v.string(), 'Hello World!'), + parameterNumber: v.nullish(v.number(), 123), + parameterBoolean: v.nullish(v.boolean(), true), parameterEnum: v.optional(v.picklist([ 'Success', 'Warning', 'Error' ])), - parameterModel: v.optional(v.union([v.optional(vModelWithString, { prop: 'Hello World!' }), v.null()])) + parameterModel: v.nullish(vModelWithString) })) }); @@ -1233,8 +1219,8 @@ parameterStringWithDefault: v.optional(v.string(), 'Hello World!'), parameterStringWithEmptyDefault: v.optional(v.string(), ''), parameterStringWithNoDefault: v.string(), - parameterStringNullableWithNoDefault: v.optional(v.union([v.string(), v.null()])), - parameterStringNullableWithDefault: v.optional(v.union([v.string(), v.null()]), null) + parameterStringNullableWithNoDefault: v.nullish(v.string()), + parameterStringNullableWithDefault: v.nullish(v.string(), null) }) }); @@ -1336,11 +1322,11 @@ body: v.optional(v.never()), path: v.optional(v.never()), query: v.object({ - parameterArrayCSV: v.union([v.array(v.string()), v.null()]), - parameterArraySSV: v.union([v.array(v.string()), v.null()]), - parameterArrayTSV: v.union([v.array(v.string()), v.null()]), - parameterArrayPipes: v.union([v.array(v.string()), v.null()]), - parameterArrayMulti: v.union([v.array(v.string()), v.null()]) + parameterArrayCSV: v.nullable(v.array(v.string())), + parameterArraySSV: v.nullable(v.array(v.string())), + parameterArrayTSV: v.nullable(v.array(v.string())), + parameterArrayPipes: v.nullable(v.array(v.string())), + parameterArrayMulti: v.nullable(v.array(v.string())) }) }); @@ -1351,17 +1337,16 @@ })), query: v.object({ parameterNumber: v.optional(v.number(), 123), - parameterString: v.optional(v.union([v.optional(v.string(), 'default'), v.null()]), 'default'), - parameterBoolean: v.optional(v.union([v.optional(v.boolean(), true), v.null()]), true), - parameterObject: v.optional(v.union([v.record(v.string(), v.unknown()), v.null()]), null), - parameterArray: v.union([v.array(v.string()), v.null()]), - parameterDictionary: v.union([v.record(v.string(), v.unknown()), v.null()]), - parameterEnum: v.union([ + parameterString: v.nullish(v.string(), 'default'), + parameterBoolean: v.nullish(v.boolean(), true), + parameterObject: v.nullish(v.record(v.string(), v.unknown()), null), + parameterArray: v.nullable(v.array(v.string())), + parameterDictionary: v.nullable(v.record(v.string(), v.unknown())), + parameterEnum: v.nullable(v.union([ v.literal('Success'), v.literal('Warning'), - v.literal('Error'), - v.null() - ]) + v.literal('Error') + ])) }) }); @@ -1375,7 +1360,7 @@ export const vUploadFileData = v.object({ body: v.string(), path: v.object({ - 'api-version': v.union([v.string(), v.null()]) + 'api-version': v.nullable(v.string()) }), query: v.optional(v.never()) }); @@ -1436,7 +1421,7 @@ export const vMultipartRequestData = v.object({ body: v.optional(v.object({ content: v.optional(v.string()), - data: v.optional(v.union([vModelWithString, v.null()])) + data: v.nullish(vModelWithString) })), path: v.optional(v.never()), query: v.optional(v.never()) @@ -1444,16 +1429,16 @@ export const vComplexParamsData = v.object({ body: v.optional(v.object({ - key: v.pipe(v.union([v.pipe(v.pipe(v.string(), v.maxLength(64), v.regex(/^[a-zA-Z0-9_]*$/)), v.readonly()), v.null()]), v.readonly()), - name: v.union([v.pipe(v.string(), v.maxLength(255)), v.null()]), + key: v.nullable(v.pipe(v.pipe(v.string(), v.maxLength(64), v.regex(/^[a-zA-Z0-9_]*$/)), v.readonly())), + name: v.nullable(v.pipe(v.string(), v.maxLength(255))), enabled: v.optional(v.boolean(), true), type: v.picklist([ 'Monkey', 'Horse', 'Bird' ]), - listOfModels: v.optional(v.union([v.array(vModelWithString), v.null()])), - listOfStrings: v.optional(v.union([v.array(v.string()), v.null()])), + listOfModels: v.nullish(v.array(vModelWithString)), + listOfStrings: v.nullish(v.array(v.string())), parameters: v.union([ vModelWithString, vModelWithEnum, @@ -1462,7 +1447,7 @@ ]), user: v.optional(v.pipe(v.object({ id: v.optional(v.pipe(v.pipe(v.number(), v.integer(), v.minValue(-2147483648, 'Invalid value: Expected int32 to be >= -2147483648'), v.maxValue(2147483647, 'Invalid value: Expected int32 to be <= 2147483647')), v.readonly())), - name: v.optional(v.pipe(v.union([v.pipe(v.string(), v.readonly()), v.null()]), v.readonly())) + name: v.nullish(v.pipe(v.string(), v.readonly())) }), v.readonly())) })), path: v.object({ -- tangled.sh