diff --git a/Cargo.lock b/Cargo.lock index 353d1406..ba2c911f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,9 +28,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" dependencies = [ "anstyle", "anstyle-parse", @@ -43,49 +43,49 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.82" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" [[package]] name = "autocfg" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "bitflags" @@ -95,9 +95,9 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "bstr" -version = "1.9.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" +checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" dependencies = [ "memchr", "serde", @@ -111,9 +111,12 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "cc" -version = "1.0.94" +version = "1.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +dependencies = [ + "shlex", +] [[package]] name = "cfg-if" @@ -132,14 +135,14 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-targets 0.52.5", + "windows-targets", ] [[package]] name = "clap" -version = "4.5.18" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" dependencies = [ "clap_builder", "clap_derive", @@ -147,9 +150,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.18" +version = "4.5.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" dependencies = [ "anstream", "anstyle", @@ -162,9 +165,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.32" +version = "4.5.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74a01f4f9ee6c066d42a1c8dedf0dcddad16c72a8981a309d6398de3a75b0c39" +checksum = "86bc73de94bc81e52f3bebec71bc4463e9748f7a59166663e32044669577b0e2" dependencies = [ "clap", ] @@ -189,9 +192,18 @@ checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "cool_asserts" @@ -204,9 +216,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crossbeam-deque" @@ -229,21 +241,21 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.19" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" [[package]] name = "dissimilar" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86e3bdc80eee6e16b2b6b0f87fbc98c04bee3455e35174c0de1a125d0688c632" +checksum = "59f8e79d1fbf76bdfbde321e902714bf6c49df88a7dda6fc682fc2979226962d" [[package]] name = "either" -version = "1.11.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "equivalent" @@ -293,9 +305,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "globset" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" dependencies = [ "aho-corasick", "bstr", @@ -306,9 +318,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" [[package]] name = "heck" @@ -318,9 +330,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -341,9 +353,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.22" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" dependencies = [ "crossbeam-deque", "globset", @@ -363,9 +375,9 @@ checksum = "0cfe9645a18782869361d9c8732246be7b410ad4e919d3609ebabdac00ba12c3" [[package]] name = "indexmap" -version = "2.2.6" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", "hashbrown", @@ -400,9 +412,9 @@ checksum = "72167d68f5fce3b8655487b8038691a3c9984ee769590f93f2a631f4ad64e4f5" [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] @@ -415,9 +427,9 @@ checksum = "e479e99b287d578ed5f6cd4c92cdf48db219088adb9c5b14f7c155b71dfba792" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "linux-raw-sys" @@ -427,15 +439,15 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "nu-ansi-term" @@ -448,33 +460,33 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "proc-macro2" -version = "1.0.80" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56dea16b0a29e94408b9aa5e2940a4eedbd128a1ba20e8f7ae60fd3d465af0e" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -501,9 +513,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.5" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -513,9 +525,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", @@ -524,15 +536,15 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" dependencies = [ "bitflags", "errno", @@ -543,9 +555,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -558,18 +570,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.197" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", @@ -578,24 +590,31 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "strsim" version = "0.11.1" @@ -604,9 +623,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.59" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a6531ffc7b071655e4ce2e04bd464c4830bb585a61cabb96cf808f05172615a" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -615,28 +634,28 @@ dependencies = [ [[package]] name = "terminal_size" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef" dependencies = [ "rustix", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] name = "thiserror" -version = "1.0.58" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.58" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", @@ -645,9 +664,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.12" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", @@ -657,18 +676,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.12" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", "serde", @@ -677,6 +696,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "topological-sort" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" + [[package]] name = "typeshare" version = "1.0.4" @@ -717,6 +742,7 @@ name = "typeshare-core" version = "1.12.0" dependencies = [ "anyhow", + "convert_case", "cool_asserts", "expect-test", "flexi_logger", @@ -729,28 +755,32 @@ dependencies = [ "quote", "syn", "thiserror", + "topological-sort", ] [[package]] name = "unicase" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.1.14" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "utf8parse" @@ -758,12 +788,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - [[package]] name = "walkdir" version = "2.5.0" @@ -776,19 +800,20 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", @@ -801,9 +826,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -811,9 +836,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", @@ -824,57 +849,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "winapi", + "windows-sys 0.59.0", ] -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-core" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.5", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", + "windows-targets", ] [[package]] @@ -883,135 +877,87 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets", ] [[package]] -name = "windows-targets" -version = "0.48.5" +name = "windows-sys" +version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-targets", ] [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.6" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] diff --git a/README.md b/README.md index 4e2e41f3..203b0865 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ _And in the darkness, compile them_ 💍 Do you like manually managing types that need to be passed through an FFI layer, so that your code doesn't archaically break at runtime? Be honest, nobody does. Typeshare is here to take that burden away from you! Leveraging the power of the `serde` library, Typeshare is a tool that converts your -Rust types into their equivalent forms in Swift, Go**, Kotlin, Scala and Typescript, keeping +Rust types into their equivalent forms in Swift, Go**, Python**, Kotlin, Scala and Typescript, keeping your cross-language codebase in sync. With automatic implementation for serialization and deserialization on both sides of the FFI, Typeshare does all the heavy lifting for you. It can even handle generics and convert effortlessly between standard libraries in different languages! **A few caveats. See [here](#a-quick-refresher-on-supported-languages) for more details. @@ -98,12 +98,13 @@ Are you getting weird deserialization issues? Did our procedural macro throw a c - Swift - Typescript - Go** +- Python** If there is a language that you want Typeshare to generate definitions for, you can either: 1. Open an issue in this repository requesting your language of choice. 2. Implement support for that language and open a PR with your implementation. We would be eternally grateful! 🙏 -** Right now, Go support is experimental. Enable the `go` feature when installing typeshare-cli if you want to use it. +** Right now, Go and Python support is experimental. Enable the `go` or `python` features, respectively, when installing typeshare-cli if you want to use these. ## Credits diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 44c9c9ed..48b37d17 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -13,6 +13,7 @@ path = "src/main.rs" [features] go = [] +python = [] [dependencies] clap = { version = "4.5", features = [ diff --git a/cli/data/tests/mappings_config.toml b/cli/data/tests/mappings_config.toml index 1ffbedcc..2bbcb330 100644 --- a/cli/data/tests/mappings_config.toml +++ b/cli/data/tests/mappings_config.toml @@ -11,4 +11,8 @@ "DateTime" = "String" [go.type_mappings] -"DateTime" = "string" \ No newline at end of file +"DateTime" = "string" + +[python.type_mappings] +"DateTime" = "datetime" +"Url" = "AnyUrl" diff --git a/cli/src/args.rs b/cli/src/args.rs index 31270f61..38ca460c 100644 --- a/cli/src/args.rs +++ b/cli/src/args.rs @@ -10,6 +10,8 @@ pub enum AvailableLanguage { Typescript, #[cfg(feature = "go")] Go, + #[cfg(feature = "python")] + Python, } #[derive(clap::Parser)] diff --git a/cli/src/config.rs b/cli/src/config.rs index 08b58a2b..0475e3bc 100644 --- a/cli/src/config.rs +++ b/cli/src/config.rs @@ -11,6 +11,13 @@ use std::{ const DEFAULT_CONFIG_FILE_NAME: &str = "typeshare.toml"; +#[derive(Default, Serialize, Deserialize, PartialEq, Eq, Debug)] +#[serde(default)] +#[cfg(feature = "python")] +pub struct PythonParams { + pub type_mappings: HashMap, +} + #[derive(Default, Serialize, Deserialize, Debug, PartialEq, Eq)] #[serde(default)] pub struct KotlinParams { @@ -64,6 +71,8 @@ pub(crate) struct Config { pub typescript: TypeScriptParams, pub kotlin: KotlinParams, pub scala: ScalaParams, + #[cfg(feature = "python")] + pub python: PythonParams, #[cfg(feature = "go")] pub go: GoParams, #[serde(skip)] @@ -160,6 +169,11 @@ mod test { assert_eq!(config.kotlin.type_mappings["DateTime"], "String"); assert_eq!(config.scala.type_mappings["DateTime"], "String"); assert_eq!(config.typescript.type_mappings["DateTime"], "string"); + #[cfg(feature = "python")] + { + assert_eq!(config.python.type_mappings["Url"], "AnyUrl"); + assert_eq!(config.python.type_mappings["DateTime"], "datetime"); + } #[cfg(feature = "go")] assert_eq!(config.go.type_mappings["DateTime"], "string"); } diff --git a/cli/src/main.rs b/cli/src/main.rs index 3a108c48..76e522ee 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -19,13 +19,13 @@ use clap_complete::Generator; use ignore::{overrides::OverrideBuilder, types::TypesBuilder, WalkBuilder}; use log::error; use rayon::iter::ParallelBridge; +use typeshare_core::language::GenericConstraints; #[cfg(feature = "go")] use typeshare_core::language::Go; +#[cfg(feature = "python")] +use typeshare_core::language::Python; use typeshare_core::{ - language::{ - CrateName, GenericConstraints, Kotlin, Language, Scala, SupportedLanguage, Swift, - TypeScript, - }, + language::{CrateName, Kotlin, Language, Scala, SupportedLanguage, Swift, TypeScript}, parser::ParsedData, }; @@ -78,6 +78,8 @@ fn main() -> anyhow::Result<()> { args::AvailableLanguage::Typescript => SupportedLanguage::TypeScript, #[cfg(feature = "go")] args::AvailableLanguage::Go => SupportedLanguage::Go, + #[cfg(feature = "python")] + args::AvailableLanguage::Python => SupportedLanguage::Python, }, }; @@ -210,6 +212,15 @@ fn language( SupportedLanguage::Go => { panic!("go support is currently experimental and must be enabled as a feature flag for typeshare-cli") } + #[cfg(feature = "python")] + SupportedLanguage::Python => Box::new(Python { + type_mappings: config.python.type_mappings, + ..Default::default() + }), + #[cfg(not(feature = "python"))] + SupportedLanguage::Python => { + panic!("python support is currently experimental and must be enabled as a feature flag for typeshare-cli") + } } } diff --git a/cli/src/parse.rs b/cli/src/parse.rs index 96130da5..8c5db4f9 100644 --- a/cli/src/parse.rs +++ b/cli/src/parse.rs @@ -61,6 +61,7 @@ fn output_file_name(language_type: SupportedLanguage, crate_name: &CrateName) -> SupportedLanguage::Scala => snake_case(), SupportedLanguage::Swift => pascal_case(), SupportedLanguage::TypeScript => snake_case(), + SupportedLanguage::Python => snake_case(), } } diff --git a/core/Cargo.toml b/core/Cargo.toml index aaa38900..72881e77 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -14,6 +14,8 @@ thiserror = "1" itertools = "0.12" lazy_format = "2" joinery = "2" +topological-sort = { version = "0.2.2"} +convert_case = { version = "0.6.0"} log.workspace = true flexi_logger.workspace = true diff --git a/core/data/tests/anonymous_struct_with_rename/output.py b/core/data/tests/anonymous_struct_with_rename/output.py new file mode 100644 index 00000000..da9bfbad --- /dev/null +++ b/core/data/tests/anonymous_struct_with_rename/output.py @@ -0,0 +1,54 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from enum import Enum +from pydantic import BaseModel, ConfigDict, Field +from typing import List, Literal, Union + + +class AnonymousStructWithRenameListInner(BaseModel): + """ + Generated type representing the anonymous struct variant `List` of the `AnonymousStructWithRename` Rust enum + """ + list: List[str] + +class AnonymousStructWithRenameLongFieldNamesInner(BaseModel): + """ + Generated type representing the anonymous struct variant `LongFieldNames` of the `AnonymousStructWithRename` Rust enum + """ + model_config = ConfigDict(populate_by_name=True) + + some_long_field_name: str + and_: bool = Field(alias="and") + but_one_more: List[str] + +class AnonymousStructWithRenameKebabCaseInner(BaseModel): + """ + Generated type representing the anonymous struct variant `KebabCase` of the `AnonymousStructWithRename` Rust enum + """ + model_config = ConfigDict(populate_by_name=True) + + another_list: List[str] = Field(alias="another-list") + camel_case_string_field: str = Field(alias="camelCaseStringField") + something_else: bool = Field(alias="something-else") + +class AnonymousStructWithRenameTypes(str, Enum): + LIST = "list" + LONG_FIELD_NAMES = "longFieldNames" + KEBAB_CASE = "kebabCase" + +class AnonymousStructWithRenameList(BaseModel): + type: Literal[AnonymousStructWithRenameTypes.LIST] = AnonymousStructWithRenameTypes.LIST + content: AnonymousStructWithRenameListInner + +class AnonymousStructWithRenameLongFieldNames(BaseModel): + type: Literal[AnonymousStructWithRenameTypes.LONG_FIELD_NAMES] = AnonymousStructWithRenameTypes.LONG_FIELD_NAMES + content: AnonymousStructWithRenameLongFieldNamesInner + +class AnonymousStructWithRenameKebabCase(BaseModel): + type: Literal[AnonymousStructWithRenameTypes.KEBAB_CASE] = AnonymousStructWithRenameTypes.KEBAB_CASE + content: AnonymousStructWithRenameKebabCaseInner + +AnonymousStructWithRename = Union[AnonymousStructWithRenameList, AnonymousStructWithRenameLongFieldNames, AnonymousStructWithRenameKebabCase] diff --git a/core/data/tests/can_apply_prefix_correctly/output.py b/core/data/tests/can_apply_prefix_correctly/output.py new file mode 100644 index 00000000..b5f7c805 --- /dev/null +++ b/core/data/tests/can_apply_prefix_correctly/output.py @@ -0,0 +1,46 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from enum import Enum +from pydantic import BaseModel +from typing import Dict, List, Literal, Union + + +class ItemDetailsFieldValue(BaseModel): + hello: str + +class AdvancedColorsTypes(str, Enum): + STRING = "String" + NUMBER = "Number" + NUMBER_ARRAY = "NumberArray" + REALLY_COOL_TYPE = "ReallyCoolType" + ARRAY_REALLY_COOL_TYPE = "ArrayReallyCoolType" + DICTIONARY_REALLY_COOL_TYPE = "DictionaryReallyCoolType" + +class AdvancedColorsString(BaseModel): + t: Literal[AdvancedColorsTypes.STRING] = AdvancedColorsTypes.STRING + c: str + +class AdvancedColorsNumber(BaseModel): + t: Literal[AdvancedColorsTypes.NUMBER] = AdvancedColorsTypes.NUMBER + c: int + +class AdvancedColorsNumberArray(BaseModel): + t: Literal[AdvancedColorsTypes.NUMBER_ARRAY] = AdvancedColorsTypes.NUMBER_ARRAY + c: List[int] + +class AdvancedColorsReallyCoolType(BaseModel): + t: Literal[AdvancedColorsTypes.REALLY_COOL_TYPE] = AdvancedColorsTypes.REALLY_COOL_TYPE + c: ItemDetailsFieldValue + +class AdvancedColorsArrayReallyCoolType(BaseModel): + t: Literal[AdvancedColorsTypes.ARRAY_REALLY_COOL_TYPE] = AdvancedColorsTypes.ARRAY_REALLY_COOL_TYPE + c: List[ItemDetailsFieldValue] + +class AdvancedColorsDictionaryReallyCoolType(BaseModel): + t: Literal[AdvancedColorsTypes.DICTIONARY_REALLY_COOL_TYPE] = AdvancedColorsTypes.DICTIONARY_REALLY_COOL_TYPE + c: Dict[str, ItemDetailsFieldValue] + +AdvancedColors = Union[AdvancedColorsString, AdvancedColorsNumber, AdvancedColorsNumberArray, AdvancedColorsReallyCoolType, AdvancedColorsArrayReallyCoolType, AdvancedColorsDictionaryReallyCoolType] diff --git a/core/data/tests/can_generate_algebraic_enum/output.py b/core/data/tests/can_generate_algebraic_enum/output.py new file mode 100644 index 00000000..f79aad55 --- /dev/null +++ b/core/data/tests/can_generate_algebraic_enum/output.py @@ -0,0 +1,79 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from enum import Enum +from pydantic import BaseModel +from typing import List, Literal, Union + + +class ItemDetailsFieldValue(BaseModel): + """ + Struct comment + """ + pass +class AdvancedColorsTypes(str, Enum): + STRING = "String" + NUMBER = "Number" + UNSIGNED_NUMBER = "UnsignedNumber" + NUMBER_ARRAY = "NumberArray" + REALLY_COOL_TYPE = "ReallyCoolType" + +class AdvancedColorsString(BaseModel): + """ + This is a case comment + """ + type: Literal[AdvancedColorsTypes.STRING] = AdvancedColorsTypes.STRING + content: str + +class AdvancedColorsNumber(BaseModel): + type: Literal[AdvancedColorsTypes.NUMBER] = AdvancedColorsTypes.NUMBER + content: int + +class AdvancedColorsUnsignedNumber(BaseModel): + type: Literal[AdvancedColorsTypes.UNSIGNED_NUMBER] = AdvancedColorsTypes.UNSIGNED_NUMBER + content: int + +class AdvancedColorsNumberArray(BaseModel): + type: Literal[AdvancedColorsTypes.NUMBER_ARRAY] = AdvancedColorsTypes.NUMBER_ARRAY + content: List[int] + +class AdvancedColorsReallyCoolType(BaseModel): + """ + Comment on the last element + """ + type: Literal[AdvancedColorsTypes.REALLY_COOL_TYPE] = AdvancedColorsTypes.REALLY_COOL_TYPE + content: ItemDetailsFieldValue + +# Enum comment +AdvancedColors = Union[AdvancedColorsString, AdvancedColorsNumber, AdvancedColorsUnsignedNumber, AdvancedColorsNumberArray, AdvancedColorsReallyCoolType] +class AdvancedColors2Types(str, Enum): + STRING = "string" + NUMBER = "number" + NUMBER_ARRAY = "number-array" + REALLY_COOL_TYPE = "really-cool-type" + +class AdvancedColors2String(BaseModel): + """ + This is a case comment + """ + type: Literal[AdvancedColors2Types.STRING] = AdvancedColors2Types.STRING + content: str + +class AdvancedColors2Number(BaseModel): + type: Literal[AdvancedColors2Types.NUMBER] = AdvancedColors2Types.NUMBER + content: int + +class AdvancedColors2NumberArray(BaseModel): + type: Literal[AdvancedColors2Types.NUMBER_ARRAY] = AdvancedColors2Types.NUMBER_ARRAY + content: List[int] + +class AdvancedColors2ReallyCoolType(BaseModel): + """ + Comment on the last element + """ + type: Literal[AdvancedColors2Types.REALLY_COOL_TYPE] = AdvancedColors2Types.REALLY_COOL_TYPE + content: ItemDetailsFieldValue + +AdvancedColors2 = Union[AdvancedColors2String, AdvancedColors2Number, AdvancedColors2NumberArray, AdvancedColors2ReallyCoolType] diff --git a/core/data/tests/can_generate_algebraic_enum_with_skipped_variants/output.py b/core/data/tests/can_generate_algebraic_enum_with_skipped_variants/output.py new file mode 100644 index 00000000..f023d26a --- /dev/null +++ b/core/data/tests/can_generate_algebraic_enum_with_skipped_variants/output.py @@ -0,0 +1,22 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from enum import Enum +from pydantic import BaseModel +from typing import Literal, Union + + +class SomeEnumTypes(str, Enum): + A = "A" + C = "C" + +class SomeEnumA(BaseModel): + type: Literal[SomeEnumTypes.A] = SomeEnumTypes.A + +class SomeEnumC(BaseModel): + type: Literal[SomeEnumTypes.C] = SomeEnumTypes.C + content: int + +SomeEnum = Union[SomeEnumA, SomeEnumC] diff --git a/core/data/tests/can_generate_anonymous_struct_with_skipped_fields/output.py b/core/data/tests/can_generate_anonymous_struct_with_skipped_fields/output.py new file mode 100644 index 00000000..bc0cd8a0 --- /dev/null +++ b/core/data/tests/can_generate_anonymous_struct_with_skipped_fields/output.py @@ -0,0 +1,48 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from enum import Enum +from pydantic import BaseModel +from typing import Literal, Union + + +class AutofilledByUsInner(BaseModel): + """ + Generated type representing the anonymous struct variant `Us` of the `AutofilledBy` Rust enum + """ + uuid: str + """ + The UUID for the fill + """ + +class AutofilledBySomethingElseInner(BaseModel): + """ + Generated type representing the anonymous struct variant `SomethingElse` of the `AutofilledBy` Rust enum + """ + uuid: str + """ + The UUID for the fill + """ + +class AutofilledByTypes(str, Enum): + US = "Us" + SOMETHING_ELSE = "SomethingElse" + +class AutofilledByUs(BaseModel): + """ + This field was autofilled by us + """ + type: Literal[AutofilledByTypes.US] = AutofilledByTypes.US + content: AutofilledByUsInner + +class AutofilledBySomethingElse(BaseModel): + """ + Something else autofilled this field + """ + type: Literal[AutofilledByTypes.SOMETHING_ELSE] = AutofilledByTypes.SOMETHING_ELSE + content: AutofilledBySomethingElseInner + +# Enum keeping track of who autofilled a field +AutofilledBy = Union[AutofilledByUs, AutofilledBySomethingElse] diff --git a/core/data/tests/can_generate_bare_string_enum/output.py b/core/data/tests/can_generate_bare_string_enum/output.py new file mode 100644 index 00000000..3fcf3636 --- /dev/null +++ b/core/data/tests/can_generate_bare_string_enum/output.py @@ -0,0 +1,12 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from enum import Enum + + +class Colors(str, Enum): + RED = "Red" + BLUE = "Blue" + GREEN = "Green" diff --git a/core/data/tests/can_generate_empty_algebraic_enum/output.py b/core/data/tests/can_generate_empty_algebraic_enum/output.py new file mode 100644 index 00000000..160b7be2 --- /dev/null +++ b/core/data/tests/can_generate_empty_algebraic_enum/output.py @@ -0,0 +1,24 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from enum import Enum +from pydantic import BaseModel +from typing import Literal, Union + + +class AddressDetails(BaseModel): + pass +class AddressTypes(str, Enum): + FIXED_ADDRESS = "FixedAddress" + NO_FIXED_ADDRESS = "NoFixedAddress" + +class AddressFixedAddress(BaseModel): + type: Literal[AddressTypes.FIXED_ADDRESS] = AddressTypes.FIXED_ADDRESS + content: AddressDetails + +class AddressNoFixedAddress(BaseModel): + type: Literal[AddressTypes.NO_FIXED_ADDRESS] = AddressTypes.NO_FIXED_ADDRESS + +Address = Union[AddressFixedAddress, AddressNoFixedAddress] diff --git a/core/data/tests/can_generate_simple_enum/output.py b/core/data/tests/can_generate_simple_enum/output.py new file mode 100644 index 00000000..3fcf3636 --- /dev/null +++ b/core/data/tests/can_generate_simple_enum/output.py @@ -0,0 +1,12 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from enum import Enum + + +class Colors(str, Enum): + RED = "Red" + BLUE = "Blue" + GREEN = "Green" diff --git a/core/data/tests/can_generate_simple_struct_with_a_comment/output.py b/core/data/tests/can_generate_simple_struct_with_a_comment/output.py new file mode 100644 index 00000000..0703f341 --- /dev/null +++ b/core/data/tests/can_generate_simple_struct_with_a_comment/output.py @@ -0,0 +1,24 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from pydantic import BaseModel, Field +from typing import List, Optional + + +class Location(BaseModel): + pass +class Person(BaseModel): + """ + This is a comment. + """ + name: str + """ + This is another comment + """ + age: int + info: Optional[str] = Field(default=None) + emails: List[str] + location: Location + diff --git a/core/data/tests/can_generate_slice_of_user_type/output.py b/core/data/tests/can_generate_slice_of_user_type/output.py new file mode 100644 index 00000000..5a523f4a --- /dev/null +++ b/core/data/tests/can_generate_slice_of_user_type/output.py @@ -0,0 +1,12 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from pydantic import BaseModel +from typing import List + + +class Video(BaseModel): + tags: List[Tag] + diff --git a/core/data/tests/can_generate_struct_with_skipped_fields/output.py b/core/data/tests/can_generate_struct_with_skipped_fields/output.py new file mode 100644 index 00000000..accea552 --- /dev/null +++ b/core/data/tests/can_generate_struct_with_skipped_fields/output.py @@ -0,0 +1,12 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from pydantic import BaseModel + + +class MyStruct(BaseModel): + a: int + c: int + diff --git a/core/data/tests/can_generate_unit_structs/output.py b/core/data/tests/can_generate_unit_structs/output.py new file mode 100644 index 00000000..c0b84425 --- /dev/null +++ b/core/data/tests/can_generate_unit_structs/output.py @@ -0,0 +1,10 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from pydantic import BaseModel + + +class UnitStruct(BaseModel): + pass diff --git a/core/data/tests/can_handle_anonymous_struct/output.py b/core/data/tests/can_handle_anonymous_struct/output.py new file mode 100644 index 00000000..e988f05d --- /dev/null +++ b/core/data/tests/can_handle_anonymous_struct/output.py @@ -0,0 +1,97 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from enum import Enum +from pydantic import BaseModel +from typing import Literal, Union + + +class AutofilledByUsInner(BaseModel): + """ + Generated type representing the anonymous struct variant `Us` of the `AutofilledBy` Rust enum + """ + uuid: str + """ + The UUID for the fill + """ + +class AutofilledBySomethingElseInner(BaseModel): + """ + Generated type representing the anonymous struct variant `SomethingElse` of the `AutofilledBy` Rust enum + """ + uuid: str + """ + The UUID for the fill + """ + thing: int + """ + Some other thing + """ + +class AutofilledByTypes(str, Enum): + US = "Us" + SOMETHING_ELSE = "SomethingElse" + +class AutofilledByUs(BaseModel): + """ + This field was autofilled by us + """ + type: Literal[AutofilledByTypes.US] = AutofilledByTypes.US + content: AutofilledByUsInner + +class AutofilledBySomethingElse(BaseModel): + """ + Something else autofilled this field + """ + type: Literal[AutofilledByTypes.SOMETHING_ELSE] = AutofilledByTypes.SOMETHING_ELSE + content: AutofilledBySomethingElseInner + +# Enum keeping track of who autofilled a field +AutofilledBy = Union[AutofilledByUs, AutofilledBySomethingElse] +class EnumWithManyVariantsAnonVariantInner(BaseModel): + """ + Generated type representing the anonymous struct variant `AnonVariant` of the `EnumWithManyVariants` Rust enum + """ + uuid: str + +class EnumWithManyVariantsAnotherAnonVariantInner(BaseModel): + """ + Generated type representing the anonymous struct variant `AnotherAnonVariant` of the `EnumWithManyVariants` Rust enum + """ + uuid: str + thing: int + +class EnumWithManyVariantsTypes(str, Enum): + UNIT_VARIANT = "UnitVariant" + TUPLE_VARIANT_STRING = "TupleVariantString" + ANON_VARIANT = "AnonVariant" + TUPLE_VARIANT_INT = "TupleVariantInt" + ANOTHER_UNIT_VARIANT = "AnotherUnitVariant" + ANOTHER_ANON_VARIANT = "AnotherAnonVariant" + +class EnumWithManyVariantsUnitVariant(BaseModel): + type: Literal[EnumWithManyVariantsTypes.UNIT_VARIANT] = EnumWithManyVariantsTypes.UNIT_VARIANT + +class EnumWithManyVariantsTupleVariantString(BaseModel): + type: Literal[EnumWithManyVariantsTypes.TUPLE_VARIANT_STRING] = EnumWithManyVariantsTypes.TUPLE_VARIANT_STRING + content: str + +class EnumWithManyVariantsAnonVariant(BaseModel): + type: Literal[EnumWithManyVariantsTypes.ANON_VARIANT] = EnumWithManyVariantsTypes.ANON_VARIANT + content: EnumWithManyVariantsAnonVariantInner + +class EnumWithManyVariantsTupleVariantInt(BaseModel): + type: Literal[EnumWithManyVariantsTypes.TUPLE_VARIANT_INT] = EnumWithManyVariantsTypes.TUPLE_VARIANT_INT + content: int + +class EnumWithManyVariantsAnotherUnitVariant(BaseModel): + type: Literal[EnumWithManyVariantsTypes.ANOTHER_UNIT_VARIANT] = EnumWithManyVariantsTypes.ANOTHER_UNIT_VARIANT + +class EnumWithManyVariantsAnotherAnonVariant(BaseModel): + type: Literal[EnumWithManyVariantsTypes.ANOTHER_ANON_VARIANT] = EnumWithManyVariantsTypes.ANOTHER_ANON_VARIANT + content: EnumWithManyVariantsAnotherAnonVariantInner + +# This is a comment (yareek sameek wuz here) +EnumWithManyVariants = Union[EnumWithManyVariantsUnitVariant, EnumWithManyVariantsTupleVariantString, EnumWithManyVariantsAnonVariant, EnumWithManyVariantsTupleVariantInt, EnumWithManyVariantsAnotherUnitVariant, EnumWithManyVariantsAnotherAnonVariant] diff --git a/core/data/tests/can_handle_quote_in_serde_rename/output.py b/core/data/tests/can_handle_quote_in_serde_rename/output.py new file mode 100644 index 00000000..3b96d390 --- /dev/null +++ b/core/data/tests/can_handle_quote_in_serde_rename/output.py @@ -0,0 +1,10 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from enum import Enum + + +class Colors(str, Enum): + GREEN = "Green\"" diff --git a/core/data/tests/can_handle_serde_rename/output.py b/core/data/tests/can_handle_serde_rename/output.py new file mode 100644 index 00000000..5d08609f --- /dev/null +++ b/core/data/tests/can_handle_serde_rename/output.py @@ -0,0 +1,24 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from pydantic import BaseModel, ConfigDict, Field +from typing import List, Optional + + +class OtherType(BaseModel): + pass +class Person(BaseModel): + """ + This is a comment. + """ + model_config = ConfigDict(populate_by_name=True) + + name: str + age: int + extra_special_field_1: int = Field(alias="extraSpecialFieldOne") + extra_special_field_2: Optional[List[str]] = Field(alias="extraSpecialFieldTwo", default=None) + non_standard_data_type: OtherType = Field(alias="nonStandardDataType") + non_standard_data_type_in_array: Optional[List[OtherType]] = Field(alias="nonStandardDataTypeInArray", default=None) + diff --git a/core/data/tests/can_handle_serde_rename_all/output.py b/core/data/tests/can_handle_serde_rename_all/output.py new file mode 100644 index 00000000..b39990ca --- /dev/null +++ b/core/data/tests/can_handle_serde_rename_all/output.py @@ -0,0 +1,31 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from pydantic import BaseModel, ConfigDict, Field +from typing import List, Optional + + +class Person(BaseModel): + """ + This is a Person struct with camelCase rename + """ + model_config = ConfigDict(populate_by_name=True) + + first_name: str = Field(alias="firstName") + last_name: str = Field(alias="lastName") + age: int + extra_special_field_1: int = Field(alias="extraSpecialField1") + extra_special_field_2: Optional[List[str]] = Field(alias="extraSpecialField2", default=None) + +class Person2(BaseModel): + """ + This is a Person2 struct with UPPERCASE rename + """ + model_config = ConfigDict(populate_by_name=True) + + first_name: str = Field(alias="FIRST_NAME") + last_name: str = Field(alias="LAST_NAME") + age: int = Field(alias="AGE") + diff --git a/core/data/tests/can_handle_serde_rename_on_top_level/output.py b/core/data/tests/can_handle_serde_rename_on_top_level/output.py new file mode 100644 index 00000000..1d27ede4 --- /dev/null +++ b/core/data/tests/can_handle_serde_rename_on_top_level/output.py @@ -0,0 +1,24 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from pydantic import BaseModel, ConfigDict, Field +from typing import List, Optional + + +class OtherType(BaseModel): + pass +class PersonTwo(BaseModel): + """ + This is a comment. + """ + model_config = ConfigDict(populate_by_name=True) + + name: str + age: int + extra_special_field_1: int = Field(alias="extraSpecialFieldOne") + extra_special_field_2: Optional[List[str]] = Field(alias="extraSpecialFieldTwo", default=None) + non_standard_data_type: OtherType = Field(alias="nonStandardDataType") + non_standard_data_type_in_array: Optional[List[OtherType]] = Field(alias="nonStandardDataTypeInArray", default=None) + diff --git a/core/data/tests/can_handle_unit_type/output.py b/core/data/tests/can_handle_unit_type/output.py new file mode 100644 index 00000000..2d74dce9 --- /dev/null +++ b/core/data/tests/can_handle_unit_type/output.py @@ -0,0 +1,27 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from enum import Enum +from pydantic import BaseModel, ConfigDict, Field +from typing import Literal + + +class StructHasVoidType(BaseModel): + """ + This struct has a unit field + """ + model_config = ConfigDict(populate_by_name=True) + + this_is_a_unit: None = Field(alias="thisIsAUnit") + +class EnumHasVoidTypeTypes(str, Enum): + HAS_A_UNIT = "hasAUnit" + +class EnumHasVoidTypeHasAUnit(BaseModel): + type: Literal[EnumHasVoidTypeTypes.HAS_A_UNIT] = EnumHasVoidTypeTypes.HAS_A_UNIT + content: None + +# This enum has a variant associated with unit data +EnumHasVoidType = EnumHasVoidTypeHasAUnit diff --git a/core/data/tests/can_recognize_types_inside_modules/output.py b/core/data/tests/can_recognize_types_inside_modules/output.py new file mode 100644 index 00000000..92e7bec8 --- /dev/null +++ b/core/data/tests/can_recognize_types_inside_modules/output.py @@ -0,0 +1,20 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from pydantic import BaseModel + + +class A(BaseModel): + field: int + +class AB(BaseModel): + field: int + +class ABC(BaseModel): + field: int + +class OutsideOfModules(BaseModel): + field: int + diff --git a/core/data/tests/enum_is_properly_named_with_serde_overrides/output.py b/core/data/tests/enum_is_properly_named_with_serde_overrides/output.py new file mode 100644 index 00000000..c25f2e3a --- /dev/null +++ b/core/data/tests/enum_is_properly_named_with_serde_overrides/output.py @@ -0,0 +1,12 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from enum import Enum + + +class Colors(str, Enum): + RED = "red" + BLUE = "blue" + GREEN = "green-like" diff --git a/core/data/tests/excluded_by_target_os/output.py b/core/data/tests/excluded_by_target_os/output.py new file mode 100644 index 00000000..dd85551f --- /dev/null +++ b/core/data/tests/excluded_by_target_os/output.py @@ -0,0 +1,68 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from enum import Enum +from pydantic import BaseModel, ConfigDict, Field +from typing import Literal, Union + + +class DefinedTwice(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + field_1: str = Field(alias="field1") + +class Excluded(BaseModel): + pass +class ManyStruct(BaseModel): + pass +class MultipleTargets(BaseModel): + pass +class NestedNotTarget1(BaseModel): + pass +class OtherExcluded(BaseModel): + pass +class SomeEnum(str, Enum): + pass +class TestEnumVariant7Inner(BaseModel): + """ + Generated type representing the anonymous struct variant `Variant7` of the `TestEnum` Rust enum + """ + model_config = ConfigDict(populate_by_name=True) + + field_1: str = Field(alias="field1") + +class TestEnumVariant9Inner(BaseModel): + """ + Generated type representing the anonymous struct variant `Variant9` of the `TestEnum` Rust enum + """ + model_config = ConfigDict(populate_by_name=True) + + field_2: str = Field(alias="field2") + +class TestEnumTypes(str, Enum): + VARIANT_1 = "Variant1" + VARIANT_5 = "Variant5" + VARIANT_7 = "Variant7" + VARIANT_8 = "Variant8" + VARIANT_9 = "Variant9" + +class TestEnumVariant1(BaseModel): + type: Literal[TestEnumTypes.VARIANT_1] = TestEnumTypes.VARIANT_1 + +class TestEnumVariant5(BaseModel): + type: Literal[TestEnumTypes.VARIANT_5] = TestEnumTypes.VARIANT_5 + +class TestEnumVariant7(BaseModel): + type: Literal[TestEnumTypes.VARIANT_7] = TestEnumTypes.VARIANT_7 + content: TestEnumVariant7Inner + +class TestEnumVariant8(BaseModel): + type: Literal[TestEnumTypes.VARIANT_8] = TestEnumTypes.VARIANT_8 + +class TestEnumVariant9(BaseModel): + type: Literal[TestEnumTypes.VARIANT_9] = TestEnumTypes.VARIANT_9 + content: TestEnumVariant9Inner + +TestEnum = Union[TestEnumVariant1, TestEnumVariant5, TestEnumVariant7, TestEnumVariant8, TestEnumVariant9] diff --git a/core/data/tests/generate_types/output.py b/core/data/tests/generate_types/output.py new file mode 100644 index 00000000..48ea1a2f --- /dev/null +++ b/core/data/tests/generate_types/output.py @@ -0,0 +1,25 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from pydantic import BaseModel, ConfigDict, Field +from typing import Dict, List, Optional + + +class CustomType(BaseModel): + pass +class Types(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + s: str + static_s: str + int_8: int = Field(alias="int8") + float: float + double: float + array: List[str] + fixed_length_array: List[str] + dictionary: Dict[str, int] + optional_dictionary: Optional[Dict[str, int]] = Field(default=None) + custom_type: CustomType + diff --git a/core/data/tests/generates_empty_structs_and_initializers/output.py b/core/data/tests/generates_empty_structs_and_initializers/output.py new file mode 100644 index 00000000..aea6ab33 --- /dev/null +++ b/core/data/tests/generates_empty_structs_and_initializers/output.py @@ -0,0 +1,10 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from pydantic import BaseModel + + +class MyEmptyStruct(BaseModel): + pass diff --git a/core/data/tests/kebab_case_rename/output.py b/core/data/tests/kebab_case_rename/output.py new file mode 100644 index 00000000..d6ccdec8 --- /dev/null +++ b/core/data/tests/kebab_case_rename/output.py @@ -0,0 +1,19 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from pydantic import BaseModel, ConfigDict, Field +from typing import Optional + + +class Things(BaseModel): + """ + This is a comment. + """ + model_config = ConfigDict(populate_by_name=True) + + bla: str + some_label: Optional[str] = Field(alias="label", default=None) + label_left: Optional[str] = Field(alias="label-left", default=None) + diff --git a/core/data/tests/orders_types/output.py b/core/data/tests/orders_types/output.py new file mode 100644 index 00000000..ba841ec2 --- /dev/null +++ b/core/data/tests/orders_types/output.py @@ -0,0 +1,33 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from pydantic import BaseModel, ConfigDict, Field +from typing import Optional + + +class A(BaseModel): + field: int + +class B(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + depends_on: A = Field(alias="dependsOn") + +class C(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + depends_on: B = Field(alias="dependsOn") + +class E(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + depends_on: D = Field(alias="dependsOn") + +class D(BaseModel): + model_config = ConfigDict(populate_by_name=True) + + depends_on: C = Field(alias="dependsOn") + also_depends_on: Optional[E] = Field(alias="alsoDependsOn", default=None) + diff --git a/core/data/tests/recursive_enum_decorator/output.py b/core/data/tests/recursive_enum_decorator/output.py new file mode 100644 index 00000000..de4b2417 --- /dev/null +++ b/core/data/tests/recursive_enum_decorator/output.py @@ -0,0 +1,58 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from enum import Enum +from pydantic import BaseModel +from typing import Literal, Union + + +class MoreOptionsExactlyInner(BaseModel): + """ + Generated type representing the anonymous struct variant `Exactly` of the `MoreOptions` Rust enum + """ + config: str + +class MoreOptionsBuiltInner(BaseModel): + """ + Generated type representing the anonymous struct variant `Built` of the `MoreOptions` Rust enum + """ + top: MoreOptions + +class MoreOptionsTypes(str, Enum): + NEWS = "news" + EXACTLY = "exactly" + BUILT = "built" + +class MoreOptionsNews(BaseModel): + type: Literal[MoreOptionsTypes.NEWS] = MoreOptionsTypes.NEWS + content: bool + +class MoreOptionsExactly(BaseModel): + type: Literal[MoreOptionsTypes.EXACTLY] = MoreOptionsTypes.EXACTLY + content: MoreOptionsExactlyInner + +class MoreOptionsBuilt(BaseModel): + type: Literal[MoreOptionsTypes.BUILT] = MoreOptionsTypes.BUILT + content: MoreOptionsBuiltInner + +MoreOptions = Union[MoreOptionsNews, MoreOptionsExactly, MoreOptionsBuilt] +class OptionsTypes(str, Enum): + RED = "red" + BANANA = "banana" + VERMONT = "vermont" + +class OptionsRed(BaseModel): + type: Literal[OptionsTypes.RED] = OptionsTypes.RED + content: bool + +class OptionsBanana(BaseModel): + type: Literal[OptionsTypes.BANANA] = OptionsTypes.BANANA + content: str + +class OptionsVermont(BaseModel): + type: Literal[OptionsTypes.VERMONT] = OptionsTypes.VERMONT + content: Options + +Options = Union[OptionsRed, OptionsBanana, OptionsVermont] diff --git a/core/data/tests/resolves_qualified_type/output.py b/core/data/tests/resolves_qualified_type/output.py new file mode 100644 index 00000000..c17b6aac --- /dev/null +++ b/core/data/tests/resolves_qualified_type/output.py @@ -0,0 +1,17 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from pydantic import BaseModel, Field +from typing import Dict, List, Optional + + +class QualifiedTypes(BaseModel): + unqualified: str + qualified: str + qualified_vec: List[str] + qualified_hashmap: Dict[str, str] + qualified_optional: Optional[str] = Field(default=None) + qualfied_optional_hashmap_vec: Optional[Dict[str, List[str]]] = Field(default=None) + diff --git a/core/data/tests/serialize_anonymous_field_as/output.py b/core/data/tests/serialize_anonymous_field_as/output.py new file mode 100644 index 00000000..921258d5 --- /dev/null +++ b/core/data/tests/serialize_anonymous_field_as/output.py @@ -0,0 +1,26 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from enum import Enum +from pydantic import BaseModel +from typing import Literal, Union + + +class SomeEnumTypes(str, Enum): + CONTEXT = "Context" + OTHER = "Other" + +class SomeEnumContext(BaseModel): + """ + The associated String contains some opaque context + """ + type: Literal[SomeEnumTypes.CONTEXT] = SomeEnumTypes.CONTEXT + content: str + +class SomeEnumOther(BaseModel): + type: Literal[SomeEnumTypes.OTHER] = SomeEnumTypes.OTHER + content: int + +SomeEnum = Union[SomeEnumContext, SomeEnumOther] diff --git a/core/data/tests/serialize_field_as/output.py b/core/data/tests/serialize_field_as/output.py new file mode 100644 index 00000000..fc6aeae0 --- /dev/null +++ b/core/data/tests/serialize_field_as/output.py @@ -0,0 +1,14 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from pydantic import BaseModel, Field +from typing import List, Optional + + +class EditItemViewModelSaveRequest(BaseModel): + context: str + values: List[EditItemSaveValue] + fill_action: Optional[AutoFillItemActionRequest] = Field(default=None) + diff --git a/core/data/tests/serialize_type_alias/output.py b/core/data/tests/serialize_type_alias/output.py new file mode 100644 index 00000000..be693ae3 --- /dev/null +++ b/core/data/tests/serialize_type_alias/output.py @@ -0,0 +1,19 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + + + + +Uuid = str + +AccountUuid = Uuid + +""" +Unique identifier for an Account +""" +AlsoString = str + +ItemUuid = str + diff --git a/core/data/tests/smart_pointers/output.py b/core/data/tests/smart_pointers/output.py new file mode 100644 index 00000000..f4fae9cf --- /dev/null +++ b/core/data/tests/smart_pointers/output.py @@ -0,0 +1,69 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from enum import Enum +from pydantic import BaseModel +from typing import List, Literal, Union + + +class ArcyColors(BaseModel): + """ + This is a comment. + """ + red: int + blue: str + green: List[str] + +class CellyColors(BaseModel): + """ + This is a comment. + """ + red: str + blue: List[str] + +class CowyColors(BaseModel): + """ + This is a comment. + """ + lifetime: str + +class LockyColors(BaseModel): + """ + This is a comment. + """ + red: str + +class MutexyColors(BaseModel): + """ + This is a comment. + """ + blue: List[str] + green: str + +class RcyColors(BaseModel): + """ + This is a comment. + """ + red: str + blue: List[str] + green: str + +class BoxyColorsTypes(str, Enum): + RED = "Red" + BLUE = "Blue" + GREEN = "Green" + +class BoxyColorsRed(BaseModel): + type: Literal[BoxyColorsTypes.RED] = BoxyColorsTypes.RED + +class BoxyColorsBlue(BaseModel): + type: Literal[BoxyColorsTypes.BLUE] = BoxyColorsTypes.BLUE + +class BoxyColorsGreen(BaseModel): + type: Literal[BoxyColorsTypes.GREEN] = BoxyColorsTypes.GREEN + content: str + +# This is a comment. +BoxyColors = Union[BoxyColorsRed, BoxyColorsBlue, BoxyColorsGreen] diff --git a/core/data/tests/test_algebraic_enum_case_name_support/output.py b/core/data/tests/test_algebraic_enum_case_name_support/output.py new file mode 100644 index 00000000..dded6ce1 --- /dev/null +++ b/core/data/tests/test_algebraic_enum_case_name_support/output.py @@ -0,0 +1,35 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from enum import Enum +from pydantic import BaseModel +from typing import List, Literal, Union + + +class ItemDetailsFieldValue(BaseModel): + pass +class AdvancedColorsTypes(str, Enum): + STRING = "string" + NUMBER = "number" + NUMBER_ARRAY = "number-array" + REALLY_COOL_TYPE = "reallyCoolType" + +class AdvancedColorsString(BaseModel): + type: Literal[AdvancedColorsTypes.STRING] = AdvancedColorsTypes.STRING + content: str + +class AdvancedColorsNumber(BaseModel): + type: Literal[AdvancedColorsTypes.NUMBER] = AdvancedColorsTypes.NUMBER + content: int + +class AdvancedColorsNumberArray(BaseModel): + type: Literal[AdvancedColorsTypes.NUMBER_ARRAY] = AdvancedColorsTypes.NUMBER_ARRAY + content: List[int] + +class AdvancedColorsReallyCoolType(BaseModel): + type: Literal[AdvancedColorsTypes.REALLY_COOL_TYPE] = AdvancedColorsTypes.REALLY_COOL_TYPE + content: ItemDetailsFieldValue + +AdvancedColors = Union[AdvancedColorsString, AdvancedColorsNumber, AdvancedColorsNumberArray, AdvancedColorsReallyCoolType] diff --git a/core/data/tests/test_generate_char/output.py b/core/data/tests/test_generate_char/output.py new file mode 100644 index 00000000..1fe4da98 --- /dev/null +++ b/core/data/tests/test_generate_char/output.py @@ -0,0 +1,11 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from pydantic import BaseModel + + +class MyType(BaseModel): + field: str + diff --git a/core/data/tests/test_i54_u53_type/output.py b/core/data/tests/test_i54_u53_type/output.py new file mode 100644 index 00000000..a7d123af --- /dev/null +++ b/core/data/tests/test_i54_u53_type/output.py @@ -0,0 +1,12 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from pydantic import BaseModel + + +class Foo(BaseModel): + a: int + b: int + diff --git a/core/data/tests/test_optional_type_alias/output.py b/core/data/tests/test_optional_type_alias/output.py new file mode 100644 index 00000000..31ac4162 --- /dev/null +++ b/core/data/tests/test_optional_type_alias/output.py @@ -0,0 +1,17 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from pydantic import BaseModel +from typing import Optional + + +OptionalU16 = Optional[int] + +OptionalU32 = Optional[int] + +class FooBar(BaseModel): + foo: OptionalU32 + bar: OptionalU16 + diff --git a/core/data/tests/test_serde_default_struct/output.py b/core/data/tests/test_serde_default_struct/output.py new file mode 100644 index 00000000..8c6849b3 --- /dev/null +++ b/core/data/tests/test_serde_default_struct/output.py @@ -0,0 +1,12 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from pydantic import BaseModel, Field +from typing import Optional + + +class Foo(BaseModel): + bar: Optional[bool] = Field(default=None) + diff --git a/core/data/tests/test_serde_url/output.py b/core/data/tests/test_serde_url/output.py new file mode 100644 index 00000000..e182c123 --- /dev/null +++ b/core/data/tests/test_serde_url/output.py @@ -0,0 +1,12 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from pydantic import BaseModel +from pydantic.networks import AnyUrl + + +class Foo(BaseModel): + url: AnyUrl + diff --git a/core/data/tests/test_serialized_as/output.py b/core/data/tests/test_serialized_as/output.py new file mode 100644 index 00000000..639fb48b --- /dev/null +++ b/core/data/tests/test_serialized_as/output.py @@ -0,0 +1,15 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + + + + +ItemId = str + +Options = str + +""" +Options that you could pick +""" diff --git a/core/data/tests/test_serialized_as_tuple/output.py b/core/data/tests/test_serialized_as_tuple/output.py new file mode 100644 index 00000000..9a1e8bea --- /dev/null +++ b/core/data/tests/test_serialized_as_tuple/output.py @@ -0,0 +1,10 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + + + + +ItemId = str + diff --git a/core/data/tests/test_simple_enum_case_name_support/output.py b/core/data/tests/test_simple_enum_case_name_support/output.py new file mode 100644 index 00000000..ae314bd0 --- /dev/null +++ b/core/data/tests/test_simple_enum_case_name_support/output.py @@ -0,0 +1,12 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from enum import Enum + + +class Colors(str, Enum): + RED = "red" + BLUE = "blue-ish" + GREEN = "Green" diff --git a/core/data/tests/test_type_alias/output.py b/core/data/tests/test_type_alias/output.py new file mode 100644 index 00000000..6d48e621 --- /dev/null +++ b/core/data/tests/test_type_alias/output.py @@ -0,0 +1,13 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from pydantic import BaseModel + + +Bar = str + +class Foo(BaseModel): + bar: Bar + diff --git a/core/data/tests/use_correct_decoded_variable_name/output.py b/core/data/tests/use_correct_decoded_variable_name/output.py new file mode 100644 index 00000000..aea6ab33 --- /dev/null +++ b/core/data/tests/use_correct_decoded_variable_name/output.py @@ -0,0 +1,10 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from pydantic import BaseModel + + +class MyEmptyStruct(BaseModel): + pass diff --git a/core/data/tests/use_correct_integer_types/output.py b/core/data/tests/use_correct_integer_types/output.py new file mode 100644 index 00000000..caebe2b1 --- /dev/null +++ b/core/data/tests/use_correct_integer_types/output.py @@ -0,0 +1,19 @@ +""" + Generated by typeshare 1.12.0 +""" +from __future__ import annotations + +from pydantic import BaseModel + + +class Foo(BaseModel): + """ + This is a comment. + """ + a: int + b: int + c: int + e: int + f: int + g: int + diff --git a/core/src/language/mod.rs b/core/src/language/mod.rs index 61e4ff67..a0ec7a78 100644 --- a/core/src/language/mod.rs +++ b/core/src/language/mod.rs @@ -21,12 +21,14 @@ use std::{ mod go; mod kotlin; +mod python; mod scala; mod swift; mod typescript; pub use go::Go; pub use kotlin::Kotlin; +pub use python::Python; pub use scala::Scala; pub use swift::GenericConstraints; pub use swift::Swift; @@ -100,13 +102,14 @@ pub enum SupportedLanguage { Scala, Swift, TypeScript, + Python, } impl SupportedLanguage { /// Returns an iterator over all supported language variants. pub fn all_languages() -> impl Iterator { use SupportedLanguage::*; - [Go, Kotlin, Scala, Swift, TypeScript].into_iter() + [Go, Kotlin, Scala, Swift, TypeScript, Python].into_iter() } /// Get the file name extension for the supported language. @@ -117,6 +120,7 @@ impl SupportedLanguage { SupportedLanguage::Scala => "scala", SupportedLanguage::Swift => "swift", SupportedLanguage::TypeScript => "ts", + SupportedLanguage::Python => "py", } } } @@ -131,6 +135,7 @@ impl FromStr for SupportedLanguage { "scala" => Ok(Self::Scala), "swift" => Ok(Self::Swift), "typescript" => Ok(Self::TypeScript), + "python" => Ok(Self::Python), _ => Err(ParseError::UnsupportedLanguage(s.into())), } } diff --git a/core/src/language/python.rs b/core/src/language/python.rs new file mode 100644 index 00000000..058fc55f --- /dev/null +++ b/core/src/language/python.rs @@ -0,0 +1,771 @@ +use crate::parser::ParsedData; +use crate::rust_types::{RustEnumShared, RustItem, RustType, RustTypeFormatError, SpecialRustType}; +use crate::topsort::topsort; +use crate::{ + language::Language, + rust_types::{RustEnum, RustEnumVariant, RustField, RustStruct, RustTypeAlias}, +}; +use std::collections::HashSet; +use std::hash::Hash; +use std::sync::OnceLock; +use std::{collections::HashMap, io::Write}; + +use super::CrateTypes; + +use convert_case::{Case, Casing}; + +// Utility function from the original author of supporting Python +// Since we won't be supporting generics right now, this function is unused and is left here for future reference +// Collect unique type vars from an enum field +// Since we explode enums into unions of types, we need to extract all of the generics +// used by each individual field +// We do this by exploring each field's type and comparing against the generics used by the enum +// itself +#[allow(dead_code)] +fn collect_generics_for_variant(variant_type: &RustType, generics: &[String]) -> Vec { + let mut all = vec![]; + match variant_type { + RustType::Generic { id, parameters } => { + if generics.contains(id) { + all.push(id.clone()) + } + // Recurse into the params for the case of `Foo(HashMap)` + for param in parameters { + all.extend(collect_generics_for_variant(param, generics)) + } + } + RustType::Simple { id } => { + if generics.contains(id) { + all.push(id.clone()) + } + } + RustType::Special(special) => match &special { + SpecialRustType::HashMap(key_type, value_type) => { + all.extend(collect_generics_for_variant(key_type, generics)); + all.extend(collect_generics_for_variant(value_type, generics)); + } + SpecialRustType::Option(some_type) => { + all.extend(collect_generics_for_variant(some_type, generics)); + } + SpecialRustType::Vec(value_type) => { + all.extend(collect_generics_for_variant(value_type, generics)); + } + _ => {} + }, + } + // Remove any duplicates + // E.g. Foo(HashMap) should only produce a single type var + dedup(&mut all); + all +} + +fn dedup(v: &mut Vec) { + // note the Copy constraint + let mut uniques = HashSet::new(); + v.retain(|e| uniques.insert(e.clone())); +} + +/// All information needed to generate Python type-code +#[derive(Default)] +pub struct Python { + /// Mappings from Rust type names to Python type names + pub type_mappings: HashMap, + /// HashMap + pub imports: HashMap>, + /// HashMap> + /// Used to lay out runtime references in the module + /// such that it can be read top to bottom + /// globals: HashMap>, + pub type_variables: HashSet, +} + +impl Language for Python { + fn type_map(&mut self) -> &HashMap { + &self.type_mappings + } + fn generate_types( + &mut self, + w: &mut dyn Write, + _imports: &CrateTypes, + data: ParsedData, + ) -> std::io::Result<()> { + self.begin_file(w, &data)?; + + let ParsedData { + structs, + enums, + aliases, + .. + } = data; + + let mut items = aliases + .into_iter() + .map(RustItem::Alias) + .chain(structs.into_iter().map(RustItem::Struct)) + .chain(enums.into_iter().map(RustItem::Enum)) + .collect::>(); + + topsort(&mut items); + + let mut body: Vec = Vec::new(); + for thing in items { + match thing { + RustItem::Enum(e) => self.write_enum(&mut body, &e)?, + RustItem::Struct(rs) => self.write_struct(&mut body, &rs)?, + RustItem::Alias(t) => self.write_type_alias(&mut body, &t)?, + }; + } + + self.write_all_imports(w)?; + + w.write_all(&body)?; + Ok(()) + } + + fn format_generic_type( + &mut self, + base: &String, + parameters: &[RustType], + generic_types: &[String], + ) -> Result { + if let Some(mapped) = self.type_map().get(base) { + Ok(mapped.into()) + } else { + let parameters: Result, RustTypeFormatError> = parameters + .iter() + .map(|p| self.format_type(p, generic_types)) + .collect(); + let parameters = parameters?; + Ok(format!( + "{}{}", + self.format_simple_type(base, generic_types)?, + (!parameters.is_empty()) + .then(|| format!("[{}]", parameters.join(", "))) + .unwrap_or_default() + )) + } + } + + fn format_simple_type( + &mut self, + base: &String, + _generic_types: &[String], + ) -> Result { + self.add_imports(base); + Ok(if let Some(mapped) = self.type_map().get(base) { + mapped.into() + } else { + base.into() + }) + } + + fn format_special_type( + &mut self, + special_ty: &SpecialRustType, + generic_types: &[String], + ) -> Result { + match special_ty { + SpecialRustType::Vec(rtype) + | SpecialRustType::Array(rtype, _) + | SpecialRustType::Slice(rtype) => { + self.add_import("typing".to_string(), "List".to_string()); + Ok(format!("List[{}]", self.format_type(rtype, generic_types)?)) + } + // We add optionality above the type formatting level + SpecialRustType::Option(rtype) => { + self.add_import("typing".to_string(), "Optional".to_string()); + Ok(format!( + "Optional[{}]", + self.format_type(rtype, generic_types)? + )) + } + SpecialRustType::HashMap(rtype1, rtype2) => { + self.add_import("typing".to_string(), "Dict".to_string()); + Ok(format!( + "Dict[{}, {}]", + match rtype1.as_ref() { + RustType::Simple { id } if generic_types.contains(id) => { + return Err(RustTypeFormatError::GenericKeyForbiddenInTS(id.clone())); + } + _ => self.format_type(rtype1, generic_types)?, + }, + self.format_type(rtype2, generic_types)? + )) + } + SpecialRustType::Unit => Ok("None".into()), + SpecialRustType::String | SpecialRustType::Char => Ok("str".into()), + SpecialRustType::I8 + | SpecialRustType::U8 + | SpecialRustType::I16 + | SpecialRustType::U16 + | SpecialRustType::I32 + | SpecialRustType::U32 + | SpecialRustType::I54 + | SpecialRustType::U53 + | SpecialRustType::U64 + | SpecialRustType::I64 + | SpecialRustType::ISize + | SpecialRustType::USize => Ok("int".into()), + SpecialRustType::F32 | SpecialRustType::F64 => Ok("float".into()), + SpecialRustType::Bool => Ok("bool".into()), + } + } + + fn begin_file(&mut self, w: &mut dyn Write, _parsed_data: &ParsedData) -> std::io::Result<()> { + writeln!(w, "\"\"\"")?; + writeln!(w, " Generated by typeshare {}", env!("CARGO_PKG_VERSION"))?; + writeln!(w, "\"\"\"")?; + Ok(()) + } + + fn write_type_alias(&mut self, w: &mut dyn Write, ty: &RustTypeAlias) -> std::io::Result<()> { + let r#type = self + .format_type(&ty.r#type, ty.generic_types.as_slice()) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + + writeln!( + w, + "{}{} = {}\n", + ty.id.renamed, + (!ty.generic_types.is_empty()) + .then(|| format!("[{}]", ty.generic_types.join(", "))) + .unwrap_or_default(), + r#type, + )?; + + self.write_comments(w, true, &ty.comments, 0)?; + + Ok(()) + } + + fn write_struct(&mut self, w: &mut dyn Write, rs: &RustStruct) -> std::io::Result<()> { + { + rs.generic_types + .iter() + .cloned() + .for_each(|v| self.add_type_var(v)) + } + let bases = match rs.generic_types.is_empty() { + true => "BaseModel".to_string(), + false => { + self.add_import("typing".to_string(), "Generic".to_string()); + format!("BaseModel, Generic[{}]", rs.generic_types.join(", ")) + } + }; + writeln!(w, "class {}({}):", rs.id.renamed, bases,)?; + + self.write_comments(w, true, &rs.comments, 1)?; + + handle_model_config(w, self, &rs.fields); + + rs.fields + .iter() + .try_for_each(|f| self.write_field(w, f, rs.generic_types.as_slice()))?; + + if rs.fields.is_empty() { + write!(w, " pass")? + } + writeln!(w)?; + self.add_import("pydantic".to_string(), "BaseModel".to_string()); + Ok(()) + } + + fn write_enum(&mut self, w: &mut dyn Write, e: &RustEnum) -> std::io::Result<()> { + // Make a suitable name for an anonymous struct enum variant + let make_anonymous_struct_name = + |variant_name: &str| format!("{}{}Inner", &e.shared().id.renamed, variant_name); + + // Generate named types for any anonymous struct variants of this enum + self.write_types_for_anonymous_structs(w, e, &make_anonymous_struct_name)?; + match e { + // Write all the unit variants out (there can only be unit variants in + // this case) + RustEnum::Unit(shared) => { + self.add_import("enum".to_string(), "Enum".to_string()); + writeln!(w, "class {}(str, Enum):", shared.id.renamed)?; + if shared.variants.is_empty() { + writeln!(w, " pass")?; + } else { + shared.variants.iter().try_for_each(|v| { + writeln!( + w, + " {} = \"{}\"", + v.shared().id.original.to_uppercase(), + match v { + RustEnumVariant::Unit(v) => { + v.id.renamed.replace("\"", "\\\"") + } + _ => panic!(), + } + ) + })? + }; + } + // Write all the algebraic variants out (all three variant types are possible + // here) + RustEnum::Algebraic { + tag_key, + content_key, + shared, + .. + } => { + self.write_algebraic_enum( + tag_key, + content_key, + &e.shared().id.renamed, + shared, + w, + &make_anonymous_struct_name, + )?; + } + }; + Ok(()) + } + + fn write_imports( + &mut self, + _writer: &mut dyn Write, + _imports: super::ScopedCrateTypes<'_>, + ) -> std::io::Result<()> { + todo!() + } +} + +impl Python { + fn add_imports(&mut self, tp: &str) { + match tp { + "Url" => { + self.add_import("pydantic.networks".to_string(), "AnyUrl".to_string()); + } + "DateTime" => { + self.add_import("datetime".to_string(), "datetime".to_string()); + } + _ => {} + } + } + + fn write_field( + &mut self, + w: &mut dyn Write, + field: &RustField, + generic_types: &[String], + ) -> std::io::Result<()> { + let is_optional = field.ty.is_optional() || field.has_default; + // currently, if a field has a serde default value, it must be an Option + let not_optional_but_default = !field.ty.is_optional() && field.has_default; + let python_type = self + .format_type(&field.ty, generic_types) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + let python_field_name = python_property_aware_rename(&field.id.original); + let is_aliased = python_field_name != field.id.renamed; + match (not_optional_but_default, is_aliased) { + (true, true) => { + self.add_import("typing".to_string(), "Optional".to_string()); + self.add_import("pydantic".to_string(), "Field".to_string()); + write!(w, " {python_field_name}: Optional[{python_type}] = Field(alias=\"{renamed}\", default=None)", renamed=field.id.renamed)?; + } + (true, false) => { + self.add_import("typing".to_string(), "Optional".to_string()); + self.add_import("pydantic".to_string(), "Field".to_string()); + writeln!( + w, + " {python_field_name}: Optional[{python_type}] = Field(default=None)" + )? + } + (false, true) => { + self.add_import("pydantic".to_string(), "Field".to_string()); + write!( + w, + " {python_field_name}: {python_type} = Field(alias=\"{renamed}\"", + renamed = field.id.renamed + )?; + if is_optional { + writeln!(w, ", default=None)")?; + } else { + writeln!(w, ")")?; + } + } + (false, false) => { + write!( + w, + " {python_field_name}: {python_type}", + python_field_name = python_field_name, + python_type = python_type + )?; + if is_optional { + self.add_import("pydantic".to_string(), "Field".to_string()); + writeln!(w, " = Field(default=None)")?; + } else { + writeln!(w)?; + } + } + } + + self.write_comments(w, true, &field.comments, 1)?; + Ok(()) + } + + fn write_comments( + &self, + w: &mut dyn Write, + is_docstring: bool, + comments: &[String], + indent_level: usize, + ) -> std::io::Result<()> { + // Only attempt to write a comment if there are some, otherwise we're Ok() + let indent = " ".repeat(indent_level); + if !comments.is_empty() { + let comment: String = { + if is_docstring { + format!( + "{indent}\"\"\"\n{indented_comments}\n{indent}\"\"\"", + indent = indent, + indented_comments = comments + .iter() + .map(|v| format!("{}{}", indent, v)) + .collect::>() + .join("\n"), + ) + } else { + comments + .iter() + .map(|v| format!("{}# {}", indent, v)) + .collect::>() + .join("\n") + } + }; + writeln!(w, "{}", comment)?; + } + Ok(()) + } + + // Idempotently insert an import + fn add_import(&mut self, module: String, identifier: String) { + self.imports.entry(module).or_default().insert(identifier); + } + + fn add_type_var(&mut self, name: String) { + self.add_import("typing".to_string(), "TypeVar".to_string()); + self.type_variables.insert(name); + } + + fn write_all_imports(&self, w: &mut dyn Write) -> std::io::Result<()> { + let mut type_var_names: Vec = self.type_variables.iter().cloned().collect(); + type_var_names.sort(); + let type_vars: Vec = type_var_names + .iter() + .map(|name| format!("{} = TypeVar(\"{}\")", name, name)) + .collect(); + let mut imports = vec![]; + for (import_module, identifiers) in &self.imports { + let mut identifier_vec = identifiers.iter().cloned().collect::>(); + identifier_vec.sort(); + imports.push(format!( + "from {} import {}", + import_module, + identifier_vec.join(", ") + )) + } + imports.sort(); + + writeln!(w, "from __future__ import annotations\n")?; + writeln!(w, "{}\n", imports.join("\n"))?; + + match type_vars.is_empty() { + true => writeln!(w)?, + false => writeln!(w, "{}\n\n", type_vars.join("\n"))?, + }; + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + fn write_variant_class( + &mut self, + class_name: &str, + tag_key: &str, + tag_value: &str, + content_key: &str, + content_type: Option<&str>, + content_value: Option<&str>, + comments: &[String], + w: &mut dyn Write, + ) -> std::io::Result<()> { + self.add_import("typing".to_string(), "Literal".to_string()); + writeln!(w, "class {class_name}(BaseModel):")?; + self.write_comments(w, true, comments, 1)?; + + writeln!(w, " {tag_key}: Literal[{tag_value}] = {tag_value}",)?; + if content_type.is_none() && content_value.is_none() { + return Ok(()); + } + writeln!( + w, + " {content_key}{}{}", + if let Some(content_type) = content_type { + format!(": {}", content_type) + } else { + "".to_string() + }, + if let Some(content_value) = content_value { + format!(" = {}", content_value) + } else { + "".to_string() + } + )?; + Ok(()) + } + fn write_algebraic_enum( + &mut self, + tag_key: &str, + content_key: &str, + enum_name: &str, + shared: &RustEnumShared, + w: &mut dyn Write, + make_struct_name: &dyn Fn(&str) -> String, + ) -> std::io::Result<()> { + shared + .generic_types + .iter() + .cloned() + .for_each(|v| self.add_type_var(v)); + self.add_import("pydantic".to_string(), "BaseModel".to_string()); + // all the types and class names for the enum variants in tuple + // (type_name, class_name) + let all_enum_variants_name = shared + .variants + .iter() + .map(|v| match v { + RustEnumVariant::Unit(v) => v.id.renamed.clone(), + RustEnumVariant::Tuple { shared, .. } => shared.id.renamed.clone(), + RustEnumVariant::AnonymousStruct { shared, .. } => shared.id.renamed.clone(), + }) + .map(|name| (name.to_case(Case::Snake).to_uppercase(), name)) + .collect::>(); + let enum_type_class_name = format!("{}Types", shared.id.renamed); + self.add_import("enum".to_string(), "Enum".to_string()); + // write "types" class: a union of all the enum variants + writeln!(w, "class {}(str, Enum):", enum_type_class_name)?; + writeln!( + w, + "{}", + all_enum_variants_name + .iter() + .map(|(type_key_name, type_string)| format!( + " {type_key_name} = \"{type_string}\"" + )) + .collect::>() + .join("\n") + )?; + writeln!(w)?; + + let mut union_members = Vec::new(); + // write each of the enum variant as a class: + for (variant, (type_key_name, ..)) in + shared.variants.iter().zip(all_enum_variants_name.iter()) + { + let variant_class_name = format!("{enum_name}{}", &variant.shared().id.original); + union_members.push(variant_class_name.clone()); + match variant { + RustEnumVariant::Unit(variant_shared) => { + self.write_variant_class( + &variant_class_name, + &tag_key, + format!("{enum_type_class_name}.{type_key_name}",).as_str(), + content_key, + None, + None, + &variant_shared.comments, + w, + )?; + writeln!(w)?; + } + RustEnumVariant::Tuple { + ty, + shared: variant_shared, + } => { + let tuple_name = self + .format_type(ty, shared.generic_types.as_slice()) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + self.write_variant_class( + &variant_class_name, + &tag_key, + format!("{enum_type_class_name}.{type_key_name}",).as_str(), + content_key, + Some(&tuple_name), + None, + &variant_shared.comments, + w, + )?; + writeln!(w)?; + } + RustEnumVariant::AnonymousStruct { + shared: variant_shared, + .. + } => { + // writing is taken care of by write_types_for_anonymous_structs in write_enum + let variant_class_inner_name = make_struct_name(&variant_shared.id.original); + + self.write_variant_class( + &variant_class_name, + &tag_key, + format!("{enum_type_class_name}.{type_key_name}",).as_str(), + content_key, + Some(&variant_class_inner_name), + None, + &variant_shared.comments, + w, + )?; + writeln!(w)?; + } + } + } + self.write_comments(w, false, &shared.comments, 0)?; + if union_members.len() == 1 { + writeln!(w, "{enum_name} = {}", union_members[0])?; + } else { + self.add_import("typing".to_string(), "Union".to_string()); + writeln!(w, "{enum_name} = Union[{}]", union_members.join(", "))?; + } + Ok(()) + } +} + +static PYTHON_KEYWORDS: OnceLock> = OnceLock::new(); + +fn get_python_keywords() -> &'static HashSet { + PYTHON_KEYWORDS.get_or_init(|| { + HashSet::from_iter( + vec![ + "False", "None", "True", "and", "as", "assert", "async", "await", "break", "class", + "continue", "def", "del", "elif", "else", "except", "finally", "for", "from", + "global", "if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", + "raise", "return", "try", "while", "with", "yield", + ] + .iter() + .map(|v| v.to_string()), + ) + }) +} + +fn python_property_aware_rename(name: &str) -> String { + let snake_name = name.to_case(Case::Snake); + match get_python_keywords().contains(&snake_name) { + true => format!("{}_", name), + false => snake_name, + } +} + +// If at least one field from within a class is changed when the serde rename is used (a.k.a the field has 2 words) then we must use aliasing and we must also use a config dict at the top level of the class. +fn handle_model_config(w: &mut dyn Write, python_module: &mut Python, fields: &[RustField]) { + let visibly_renamed_field = fields.iter().find(|f| { + let python_field_name = python_property_aware_rename(&f.id.original); + python_field_name != f.id.renamed + }); + if visibly_renamed_field.is_some() { + python_module.add_import("pydantic".to_string(), "ConfigDict".to_string()); + let _ = writeln!(w, " model_config = ConfigDict(populate_by_name=True)\n"); + }; +} + +#[cfg(test)] +mod test { + use crate::rust_types::Id; + + use super::*; + #[test] + fn test_python_property_aware_rename() { + assert_eq!(python_property_aware_rename("class"), "class_"); + assert_eq!(python_property_aware_rename("snake_case"), "snake_case"); + } + + #[test] + fn test_optional_value_with_serde_default() { + let mut python = Python::default(); + let mock_writer = &mut Vec::new(); + let rust_field = RustField { + id: Id { + original: "field".to_string(), + renamed: "field".to_string(), + }, + ty: RustType::Special(SpecialRustType::Option(Box::new(RustType::Simple { + id: "str".to_string(), + }))), + has_default: true, + comments: Default::default(), + decorators: Default::default(), + }; + python.write_field(mock_writer, &rust_field, &[]).unwrap(); + assert_eq!( + String::from_utf8_lossy(mock_writer), + " field: Optional[str] = Field(default=None)\n" + ); + } + + #[test] + fn test_optional_value_no_serde_default() { + let mut python = Python::default(); + let mock_writer = &mut Vec::new(); + let rust_field = RustField { + id: Id { + original: "field".to_string(), + renamed: "field".to_string(), + }, + ty: RustType::Special(SpecialRustType::Option(Box::new(RustType::Simple { + id: "str".to_string(), + }))), + has_default: false, + comments: Default::default(), + decorators: Default::default(), + }; + python.write_field(mock_writer, &rust_field, &[]).unwrap(); + assert_eq!( + String::from_utf8_lossy(mock_writer), + " field: Optional[str] = Field(default=None)\n" + ); + } + + #[test] + fn test_non_optional_value_with_serde_default() { + // technically an invalid case at the moment, as we don't support serde default values other than None + // TODO: change this test if we do + let mut python = Python::default(); + let mock_writer = &mut Vec::new(); + let rust_field = RustField { + id: Id { + original: "field".to_string(), + renamed: "field".to_string(), + }, + ty: RustType::Simple { + id: "str".to_string(), + }, + has_default: true, + comments: Default::default(), + decorators: Default::default(), + }; + python.write_field(mock_writer, &rust_field, &[]).unwrap(); + assert_eq!( + String::from_utf8_lossy(mock_writer), + " field: Optional[str] = Field(default=None)\n" + ); + } + + #[test] + fn test_non_optional_value_with_no_serde_default() { + let mut python = Python::default(); + let mock_writer = &mut Vec::new(); + let rust_field = RustField { + id: Id { + original: "field".to_string(), + renamed: "field".to_string(), + }, + ty: RustType::Simple { + id: "str".to_string(), + }, + has_default: false, + comments: Default::default(), + decorators: Default::default(), + }; + python.write_field(mock_writer, &rust_field, &[]).unwrap(); + assert_eq!(String::from_utf8_lossy(mock_writer), " field: str\n"); + } +} diff --git a/core/tests/snapshot_tests.rs b/core/tests/snapshot_tests.rs index 6d9b887f..4bf277ca 100644 --- a/core/tests/snapshot_tests.rs +++ b/core/tests/snapshot_tests.rs @@ -138,6 +138,9 @@ macro_rules! output_file_for_ident { (go) => { "output.go" }; + (python) => { + "output.py" + }; } /// Simplifies the construction of `Language` instances for each language. @@ -191,6 +194,20 @@ macro_rules! language_instance { }) }; + // Default Python + (python) => { + language_instance!(python { }) + }; + + // python with configuration fields forwarded + (python {$($field:ident: $val:expr),* $(,)?}) => { + #[allow(clippy::needless_update)] + Box::new(typeshare_core::language::Python { + $($field: $val,)* + ..Default::default() + }) + }; + // Default scala (scala) => { language_instance!(scala { @@ -406,6 +423,13 @@ static GO_MAPPINGS: Lazy> = Lazy::new(|| { .collect() }); +static PYTHON_MAPPINGS: Lazy> = Lazy::new(|| { + [("Url", "AnyUrl"), ("DateTime", "datetime")] + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect() +}); + tests! { /// Enums can_generate_algebraic_enum: [ @@ -421,7 +445,8 @@ tests! { module_name: "colorsModule".to_string(), }, typescript, - go + go, + python ]; can_generate_generic_enum: [ swift { @@ -438,7 +463,7 @@ tests! { }, kotlin, scala, - typescript + typescript, ]; can_generate_generic_type_alias: [ swift { @@ -448,7 +473,7 @@ tests! { scala, typescript ]; - can_generate_slice_of_user_type: [swift, kotlin, scala, typescript, go]; + can_generate_slice_of_user_type: [swift, kotlin, scala, typescript, go, python]; can_generate_readonly_fields: [ typescript ]; @@ -459,16 +484,17 @@ tests! { kotlin, scala, typescript, - go + go, + python ]; - can_generate_bare_string_enum: [swift, kotlin, scala, typescript, go ]; + can_generate_bare_string_enum: [swift, kotlin, scala, typescript, go, python ]; can_generate_double_option_pattern: [ typescript ]; can_recognize_types_inside_modules: [ - swift, kotlin, scala, typescript, go + swift, kotlin, scala, typescript, go, python ]; - test_simple_enum_case_name_support: [swift, kotlin, scala, typescript, go ]; + test_simple_enum_case_name_support: [swift, kotlin, scala, typescript, go, python ]; test_algebraic_enum_case_name_support: [ swift { prefix: "OP".to_string(), @@ -482,16 +508,17 @@ tests! { module_name: "colorModule".to_string(), }, typescript, - go + go, + python ]; - can_apply_prefix_correctly: [ swift { prefix: "OP".to_string(), }, kotlin { prefix: "OP".to_string(), }, scala, typescript, go ]; - can_generate_empty_algebraic_enum: [ swift { prefix: "OP".to_string(), }, kotlin { prefix: "OP".to_string(), }, scala, typescript, go ]; - can_generate_algebraic_enum_with_skipped_variants: [swift, kotlin, scala, typescript, go]; - can_generate_struct_with_skipped_fields: [swift, kotlin, scala, typescript, go]; - enum_is_properly_named_with_serde_overrides: [swift, kotlin, scala, typescript, go]; - can_handle_quote_in_serde_rename: [swift, kotlin, scala, typescript, go]; - can_handle_anonymous_struct: [swift, kotlin, scala, typescript, go]; - test_generate_char: [swift, kotlin, scala, typescript, go]; + can_apply_prefix_correctly: [ swift { prefix: "OP".to_string(), }, kotlin { prefix: "OP".to_string(), }, scala, typescript, go, python ]; + can_generate_empty_algebraic_enum: [ swift { prefix: "OP".to_string(), }, kotlin { prefix: "OP".to_string(), }, scala, typescript, go, python ]; + can_generate_algebraic_enum_with_skipped_variants: [swift, kotlin, scala, typescript, go, python]; + can_generate_struct_with_skipped_fields: [swift, kotlin, scala, typescript, go, python]; + enum_is_properly_named_with_serde_overrides: [swift, kotlin, scala, typescript, go, python]; + can_handle_quote_in_serde_rename: [swift, kotlin, scala, typescript, go, python]; + can_handle_anonymous_struct: [swift, kotlin, scala, typescript, go, python]; + test_generate_char: [swift, kotlin, scala, typescript, go, python]; anonymous_struct_with_rename: [ swift { prefix: "Core".to_string(), @@ -499,13 +526,14 @@ tests! { kotlin, scala, typescript, - go + go, + python ]; can_override_types: [swift, kotlin, scala, typescript, go]; /// Structs - can_generate_simple_struct_with_a_comment: [kotlin, swift, typescript, scala, go]; - generate_types: [kotlin, swift, typescript, scala, go]; + can_generate_simple_struct_with_a_comment: [kotlin, swift, typescript, scala, go, python]; + generate_types: [kotlin, swift, typescript, scala, go, python]; can_handle_serde_rename: [ swift { prefix: "TypeShareX_".to_string(), @@ -513,14 +541,15 @@ tests! { kotlin, scala, typescript, - go + go, + python ]; // TODO: kotlin and typescript don't appear to support this yet - generates_empty_structs_and_initializers: [swift, kotlin, scala, typescript, go]; + generates_empty_structs_and_initializers: [swift, kotlin, scala, typescript, go,python]; test_default_decorators: [swift { default_decorators: vec!["Sendable".into(), "Identifiable".into()]}]; test_default_generic_constraints: [swift { default_generic_constraints: typeshare_core::language::GenericConstraints::from_config(vec!["Sendable".into(), "Identifiable".into()]) }]; - test_i54_u53_type: [swift, kotlin, scala, typescript, go]; - test_serde_default_struct: [swift, kotlin, scala, typescript, go]; + test_i54_u53_type: [swift, kotlin, scala, typescript, go, python]; + test_serde_default_struct: [swift, kotlin, scala, typescript, go, python]; test_serde_iso8601: [ swift { prefix: String::new(), @@ -565,10 +594,13 @@ tests! { type_mappings: super::GO_MAPPINGS.clone(), uppercase_acronyms: vec!["URL".to_string()], }, + python{ + type_mappings: super::PYTHON_MAPPINGS.clone() + } ]; - test_type_alias: [ swift { prefix: "OP".to_string(), }, kotlin, scala, typescript, go ]; - test_optional_type_alias: [swift, kotlin, scala, typescript, go]; - test_serialized_as: [ swift { prefix: "OP".to_string(), }, kotlin, scala, typescript, go ]; + test_type_alias: [ swift { prefix: "OP".to_string(), }, kotlin, scala, typescript, go, python ]; + test_optional_type_alias: [swift, kotlin, scala, typescript, go, python]; + test_serialized_as: [ swift { prefix: "OP".to_string(), }, kotlin, scala, typescript, go, python ]; test_serialized_as_tuple: [ swift { prefix: "OP".to_string(), @@ -579,32 +611,33 @@ tests! { go { uppercase_acronyms: vec!["ID".to_string()], }, + python ]; - can_handle_serde_rename_all: [swift, kotlin, scala, typescript, go]; - can_handle_serde_rename_on_top_level: [swift { prefix: "OP".to_string(), }, kotlin, scala, typescript, go]; - can_generate_unit_structs: [swift, kotlin, scala, typescript, go]; - kebab_case_rename: [swift, kotlin, scala, typescript, go]; + can_handle_serde_rename_all: [swift, kotlin, scala, typescript, go,python]; + can_handle_serde_rename_on_top_level: [swift { prefix: "OP".to_string(), }, kotlin, scala, typescript, go, python]; + can_generate_unit_structs: [swift, kotlin, scala, typescript, go, python]; + kebab_case_rename: [swift, kotlin, scala, typescript, go, python]; /// Globals get topologically sorted - orders_types: [swift, kotlin, go]; + orders_types: [swift, kotlin, go, python]; /// Other - use_correct_integer_types: [swift, kotlin, scala, typescript, go]; + use_correct_integer_types: [swift, kotlin, scala, typescript, go, python]; // Only swift supports generating types with keywords generate_types_with_keywords: [swift]; // TODO: how is this different from generates_empty_structs_and_initializers? - use_correct_decoded_variable_name: [swift, kotlin, scala, typescript, go]; - can_handle_unit_type: [swift { codablevoid_constraints: vec!["Equatable".into()]} , kotlin, scala, typescript, go]; + use_correct_decoded_variable_name: [swift, kotlin, scala, typescript, go, python]; + can_handle_unit_type: [swift { codablevoid_constraints: vec!["Equatable".into()]} , kotlin, scala, typescript, go, python]; //3 tests for adding decorators to enums and structs const_enum_decorator: [ swift{ prefix: "OP".to_string(), } ]; algebraic_enum_decorator: [ swift{ prefix: "OP".to_string(), } ]; struct_decorator: [ kotlin, swift{ prefix: "OP".to_string(), } ]; - serialize_field_as: [kotlin, swift, typescript, scala, go]; - serialize_type_alias: [kotlin, swift, typescript, scala, go]; - serialize_anonymous_field_as: [kotlin, swift, typescript, scala, go]; - smart_pointers: [kotlin, swift, typescript, scala, go]; - recursive_enum_decorator: [kotlin, swift, typescript, scala, go]; + serialize_field_as: [kotlin, swift, typescript, scala, go, python]; + serialize_type_alias: [kotlin, swift, typescript, scala, go, python]; + serialize_anonymous_field_as: [kotlin, swift, typescript, scala, go, python]; + smart_pointers: [kotlin, swift, typescript, scala, go, python]; + recursive_enum_decorator: [kotlin, swift, typescript, scala, go, python]; uppercase_go_acronyms: [ go { @@ -618,10 +651,11 @@ tests! { typescript, kotlin, scala, - go + go, + python ]; - can_generate_anonymous_struct_with_skipped_fields: [swift, kotlin, scala, typescript, go]; + can_generate_anonymous_struct_with_skipped_fields: [swift, kotlin, scala, typescript, go, python]; generic_struct_with_constraints_and_decorators: [swift { codablevoid_constraints: vec!["Equatable".into()] }]; - excluded_by_target_os: [ swift, kotlin, scala, typescript, go ] target_os: ["android", "macos"]; + excluded_by_target_os: [ swift, kotlin, scala, typescript, go,python ] target_os: ["android", "macos"]; // excluded_by_target_os_full_module: [swift] target_os: "ios"; }