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({