diff --git a/temporalio/Cargo.lock b/temporalio/Cargo.lock index 1fc3849c..1ef1433e 100644 --- a/temporalio/Cargo.lock +++ b/temporalio/Cargo.lock @@ -356,21 +356,11 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "bzip2" -version = "0.5.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +checksum = "bea8dcd42434048e4f7a304411d9273a411f647446c1234a65ce0554923f4cff" dependencies = [ - "bzip2-sys", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.13+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" -dependencies = [ - "cc", - "pkg-config", + "libbz2-rs-sys", ] [[package]] @@ -585,21 +575,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crc" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - [[package]] name = "crc32fast" version = "1.5.0" @@ -611,9 +586,9 @@ dependencies = [ [[package]] name = "criterion" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bf7af66b0989381bd0be551bd7cc91912a655a58c6918420c9527b1fd8b4679" +checksum = "e1c047a62b0cc3e145fa84415a3191f628e980b194c2755aa12300a4e6cbd928" dependencies = [ "anes", "cast", @@ -635,12 +610,12 @@ dependencies = [ [[package]] name = "criterion-plot" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +checksum = "9b1bcc0dc7dfae599d84ad0b1a55f80cde8af3725da8313b528da95ef783e338" dependencies = [ "cast", - "itertools 0.10.5", + "itertools 0.13.0", ] [[package]] @@ -842,23 +817,23 @@ dependencies = [ [[package]] name = "dirs" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.48.0", + "windows-sys 0.60.2", ] [[package]] @@ -979,6 +954,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -1176,9 +1152,9 @@ checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "governor" -version = "0.8.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be93b4ec2e4710b04d9264c0c7350cdd62a8c20e5e4ac732552ebb8f0debe8eb" +checksum = "444405bbb1a762387aa22dd569429533b54a1d8759d35d3b64cb39b0293eaa19" dependencies = [ "cfg-if", "dashmap", @@ -1186,7 +1162,7 @@ dependencies = [ "futures-timer", "futures-util", "getrandom 0.3.3", - "no-std-compat", + "hashbrown 0.15.5", "nonzero_ext", "parking_lot", "portable-atomic", @@ -1599,15 +1575,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.12.1" @@ -1673,6 +1640,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "libbz2-rs-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" + [[package]] name = "libc" version = "0.2.175" @@ -1689,6 +1662,26 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "liblzma" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10bf66f4598dc77ff96677c8e763655494f00ff9c1cf79e2eb5bb07bc31f807d" +dependencies = [ + "liblzma-sys", +] + +[[package]] +name = "liblzma-sys" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "libredox" version = "0.1.9" @@ -1700,6 +1693,15 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840db8cf39d9ec4dd794376f38acc40d0fc65eec2a8f484f7fd375b84602becd" +dependencies = [ + "zlib-rs", +] + [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -1730,9 +1732,9 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "lru" -version = "0.13.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" +checksum = "86ea4e65087ff52f3862caff188d489f1fab49a0cb09e01b2e3f1a617b10aaed" dependencies = [ "hashbrown 0.15.5", ] @@ -1743,27 +1745,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "lzma-rs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" -dependencies = [ - "byteorder", - "crc", -] - -[[package]] -name = "lzma-sys" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" -dependencies = [ - "cc", - "libc", - "pkg-config", -] - [[package]] name = "magnus" version = "0.7.1" @@ -1878,12 +1859,6 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" -[[package]] -name = "no-std-compat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" - [[package]] name = "nom" version = "7.1.3" @@ -1933,6 +1908,25 @@ dependencies = [ "autocfg", ] +[[package]] +name = "objc2-core-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +dependencies = [ + "bitflags", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" +dependencies = [ + "libc", + "objc2-core-foundation", +] + [[package]] name = "object" version = "0.36.7" @@ -2202,6 +2196,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppmd-rust" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c834641d8ad1b348c9ee86dec3b9840d805acd5f24daa5f90c788951a52ff59b" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2612,13 +2612,13 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.4.6" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.16", "libredox", - "thiserror 1.0.69", + "thiserror 2.0.16", ] [[package]] @@ -2725,21 +2725,20 @@ dependencies = [ [[package]] name = "rstest" -version = "0.25.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fc39292f8613e913f7df8fa892b8944ceb47c247b78e1b1ae2f09e019be789d" +checksum = "f5a3193c063baaa2a95a33f03035c8a72b83d97a54916055ba22d35ed3839d49" dependencies = [ "futures-timer", "futures-util", "rstest_macros", - "rustc_version", ] [[package]] name = "rstest_macros" -version = "0.25.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f168d99749d307be9de54d23fd226628d99768225ef08f6ffb52e0182a27746" +checksum = "9c845311f0ff7951c5506121a9ad75aec44d083c31583b2ea5a30bcb0b0abba0" dependencies = [ "cfg-if", "glob", @@ -2968,15 +2967,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - [[package]] name = "serde_spanned" version = "1.0.0" @@ -3152,14 +3142,15 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.33.1" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" +checksum = "07cec4dc2d2e357ca1e610cfb07de2fa7a10fc3e9fe89f72545f3d244ea87753" dependencies = [ - "core-foundation-sys", "libc", "memchr", "ntapi", + "objc2-core-foundation", + "objc2-io-kit", "windows", ] @@ -3256,6 +3247,7 @@ dependencies = [ "assert_matches", "async-trait", "bimap", + "bytes", "clap", "console-subscriber", "criterion", @@ -3292,6 +3284,7 @@ dependencies = [ "ringbuf", "rstest", "rustfsm", + "semver", "serde", "serde_json", "siphasher", @@ -3302,7 +3295,6 @@ dependencies = [ "temporal-sdk", "temporal-sdk-core-api", "temporal-sdk-core-protos", - "temporal-sdk-core-test-utils", "thiserror 2.0.16", "tokio", "tokio-stream", @@ -3330,7 +3322,7 @@ dependencies = [ "tempfile", "temporal-sdk-core-protos", "thiserror 2.0.16", - "toml 0.8.23", + "toml", "tonic 0.13.1", "tracing", "tracing-core", @@ -3358,32 +3350,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "temporal-sdk-core-test-utils" -version = "0.1.0" -dependencies = [ - "anyhow", - "assert_matches", - "async-trait", - "bytes", - "futures-util", - "http-body-util", - "hyper", - "hyper-util", - "parking_lot", - "prost", - "rand 0.9.2", - "semver", - "temporal-client", - "temporal-sdk", - "temporal-sdk-core", - "temporal-sdk-core-api", - "temporal-sdk-core-protos", - "tokio", - "tracing", - "url", -] - [[package]] name = "temporalio_bridge" version = "0.1.0" @@ -3589,18 +3555,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit", -] - [[package]] name = "toml" version = "0.9.5" @@ -3609,7 +3563,7 @@ checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" dependencies = [ "indexmap 2.10.0", "serde", - "serde_spanned 1.0.0", + "serde_spanned", "toml_datetime 0.7.0", "toml_parser", "toml_writer", @@ -3621,9 +3575,6 @@ name = "toml_datetime" version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] [[package]] name = "toml_datetime" @@ -3641,10 +3592,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap 2.10.0", - "serde", - "serde_spanned 0.6.9", "toml_datetime 0.6.11", - "toml_write", "winnow", ] @@ -3657,12 +3605,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - [[package]] name = "toml_writer" version = "1.0.2" @@ -3881,7 +3823,7 @@ dependencies = [ "serde_json", "target-triple", "termcolor", - "toml 0.9.5", + "toml", ] [[package]] @@ -4155,31 +4097,55 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.57.0" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-link", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ "windows-core", - "windows-targets 0.52.6", ] [[package]] name = "windows-core" -version = "0.57.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", + "windows-link", "windows-result", - "windows-targets 0.52.6", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", ] [[package]] name = "windows-implement" -version = "0.57.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", @@ -4188,9 +4154,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.57.0" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", @@ -4203,22 +4169,32 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core", + "windows-link", +] + [[package]] name = "windows-result" -version = "0.1.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-targets 0.52.6", + "windows-link", ] [[package]] -name = "windows-sys" -version = "0.48.0" +name = "windows-strings" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-targets 0.48.5", + "windows-link", ] [[package]] @@ -4248,21 +4224,6 @@ dependencies = [ "windows-targets 0.53.3", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -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", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -4297,10 +4258,13 @@ dependencies = [ ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" +name = "windows-threading" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] [[package]] name = "windows_aarch64_gnullvm" @@ -4314,12 +4278,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -4332,12 +4290,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -4362,12 +4314,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" -[[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.6" @@ -4380,12 +4326,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -4398,12 +4338,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" -[[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.6" @@ -4416,12 +4350,6 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -4468,15 +4396,6 @@ dependencies = [ "rustix", ] -[[package]] -name = "xz2" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" -dependencies = [ - "lzma-sys", -] - [[package]] name = "yoke" version = "0.8.0" @@ -4597,34 +4516,37 @@ dependencies = [ [[package]] name = "zip" -version = "2.4.2" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" dependencies = [ "aes", "arbitrary", "bzip2", "constant_time_eq", "crc32fast", - "crossbeam-utils", "deflate64", - "displaydoc", "flate2", "getrandom 0.3.3", "hmac", "indexmap 2.10.0", - "lzma-rs", + "liblzma", "memchr", "pbkdf2", + "ppmd-rust", "sha1", - "thiserror 2.0.16", "time", - "xz2", "zeroize", "zopfli", "zstd", ] +[[package]] +name = "zlib-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f06ae92f42f5e5c42443fd094f245eb656abf56dd7cce9b8b263236565e00f2" + [[package]] name = "zopfli" version = "0.8.2" diff --git a/temporalio/ext/Cargo.toml b/temporalio/ext/Cargo.toml index cd729168..73eb3475 100644 --- a/temporalio/ext/Cargo.toml +++ b/temporalio/ext/Cargo.toml @@ -17,7 +17,7 @@ prost = "0.13" rb-sys = "0.9" temporal-client = { version = "0.1.0", path = "./sdk-core/client" } temporal-sdk-core = { version = "0.1.0", path = "./sdk-core/core", features = ["ephemeral-server"] } -temporal-sdk-core-api = { version = "0.1.0", path = "./sdk-core/core-api" } +temporal-sdk-core-api = { version = "0.1.0", path = "./sdk-core/core-api", features = ["envconfig"] } temporal-sdk-core-protos = { version = "0.1.0", path = "./sdk-core/sdk-core-protos" } tokio = "1.37" tokio-stream = "0.1" diff --git a/temporalio/ext/sdk-core b/temporalio/ext/sdk-core index eb74c70c..1807534c 160000 --- a/temporalio/ext/sdk-core +++ b/temporalio/ext/sdk-core @@ -1 +1 @@ -Subproject commit eb74c70c2f4b6d3f56f364b06d6bbc09d01be809 +Subproject commit 1807534c63c64a87716230467f47c3fd36fc9727 diff --git a/temporalio/ext/src/client.rs b/temporalio/ext/src/client.rs index 7068bab6..5e032f8d 100644 --- a/temporalio/ext/src/client.rs +++ b/temporalio/ext/src/client.rs @@ -237,8 +237,11 @@ impl Client { self.invoke_rpc(service, callback, call) } - pub fn update_metadata(&self, headers: HashMap) { - self.core.get_client().set_headers(headers); + pub fn update_metadata(&self, headers: HashMap) -> Result<(), Error> { + self.core + .get_client() + .set_headers(headers) + .map_err(|err| error!("Invalid headers: {}", err)) } pub fn update_api_key(&self, api_key: Option) { diff --git a/temporalio/ext/src/envconfig.rs b/temporalio/ext/src/envconfig.rs new file mode 100644 index 00000000..9352ebce --- /dev/null +++ b/temporalio/ext/src/envconfig.rs @@ -0,0 +1,241 @@ +use std::collections::HashMap; + +use magnus::{Error, RHash, RString, Ruby, class, function, prelude::*, scan_args}; +use temporal_sdk_core_api::envconfig::{ + ClientConfig as CoreClientConfig, ClientConfigCodec, + ClientConfigProfile as CoreClientConfigProfile, ClientConfigTLS as CoreClientConfigTLS, + DataSource, LoadClientConfigOptions, LoadClientConfigProfileOptions, + load_client_config as core_load_client_config, + load_client_config_profile as core_load_client_config_profile, +}; + +use crate::{ROOT_MOD, error}; + +pub fn init(ruby: &Ruby) -> Result<(), Error> { + let root_mod = ruby.get_inner(&ROOT_MOD); + + let class = root_mod.define_class("EnvConfig", class::object())?; + class.define_singleton_method("load_client_config", function!(load_client_config, -1))?; + class.define_singleton_method( + "load_client_connect_config", + function!(load_client_connect_config, -1), + )?; + + Ok(()) +} + +fn data_source_to_hash(ruby: &Ruby, ds: &DataSource) -> Result { + let hash = RHash::new(); + match ds { + DataSource::Path(p) => { + hash.aset(ruby.sym_new("path"), ruby.str_new(p))?; + } + DataSource::Data(d) => { + hash.aset(ruby.sym_new("data"), ruby.str_from_slice(d))?; + } + } + Ok(hash) +} + +fn tls_to_hash(ruby: &Ruby, tls: &CoreClientConfigTLS) -> Result { + let hash = RHash::new(); + hash.aset(ruby.sym_new("disabled"), tls.disabled)?; + + if let Some(v) = &tls.client_cert { + hash.aset(ruby.sym_new("client_cert"), data_source_to_hash(ruby, v)?)?; + } + if let Some(v) = &tls.client_key { + hash.aset(ruby.sym_new("client_key"), data_source_to_hash(ruby, v)?)?; + } + if let Some(v) = &tls.server_ca_cert { + hash.aset( + ruby.sym_new("server_ca_cert"), + data_source_to_hash(ruby, v)?, + )?; + } + if let Some(v) = &tls.server_name { + hash.aset(ruby.sym_new("server_name"), ruby.str_new(v))?; + } + hash.aset( + ruby.sym_new("disable_host_verification"), + tls.disable_host_verification, + )?; + + Ok(hash) +} + +fn codec_to_hash(ruby: &Ruby, codec: &ClientConfigCodec) -> Result { + let hash = RHash::new(); + if let Some(v) = &codec.endpoint { + hash.aset(ruby.sym_new("endpoint"), ruby.str_new(v))?; + } + if let Some(v) = &codec.auth { + hash.aset(ruby.sym_new("auth"), ruby.str_new(v))?; + } + Ok(hash) +} + +fn profile_to_hash(ruby: &Ruby, profile: &CoreClientConfigProfile) -> Result { + let hash = RHash::new(); + + if let Some(v) = &profile.address { + hash.aset(ruby.sym_new("address"), ruby.str_new(v))?; + } + if let Some(v) = &profile.namespace { + hash.aset(ruby.sym_new("namespace"), ruby.str_new(v))?; + } + if let Some(v) = &profile.api_key { + hash.aset(ruby.sym_new("api_key"), ruby.str_new(v))?; + } + if let Some(tls) = &profile.tls { + hash.aset(ruby.sym_new("tls"), tls_to_hash(ruby, tls)?)?; + } + if let Some(codec) = &profile.codec { + hash.aset(ruby.sym_new("codec"), codec_to_hash(ruby, codec)?)?; + } + if !profile.grpc_meta.is_empty() { + let grpc_meta_hash = RHash::new(); + for (k, v) in &profile.grpc_meta { + grpc_meta_hash.aset(ruby.str_new(k), ruby.str_new(v))?; + } + hash.aset(ruby.sym_new("grpc_meta"), grpc_meta_hash)?; + } + + Ok(hash) +} + +fn core_config_to_hash(ruby: &Ruby, core_config: &CoreClientConfig) -> Result { + let profiles_hash = RHash::new(); + for (name, profile) in &core_config.profiles { + let profile_hash = profile_to_hash(ruby, profile)?; + profiles_hash.aset(ruby.str_new(name), profile_hash)?; + } + Ok(profiles_hash) +} + +fn load_client_config_inner( + ruby: &Ruby, + config_source: Option, + config_file_strict: bool, + disable_file: bool, + env_vars: Option>, +) -> Result { + let core_config = if disable_file { + CoreClientConfig::default() + } else { + let options = LoadClientConfigOptions { + config_source, + config_file_strict, + }; + core_load_client_config(options, env_vars.as_ref()) + .map_err(|e| error!("EnvConfig error: {}", e))? + }; + + core_config_to_hash(ruby, &core_config) +} + +fn load_client_connect_config_inner( + ruby: &Ruby, + config_source: Option, + profile: Option, + disable_file: bool, + disable_env: bool, + config_file_strict: bool, + env_vars: Option>, +) -> Result { + let options = LoadClientConfigProfileOptions { + config_source, + config_file_profile: profile, + config_file_strict, + disable_file, + disable_env, + }; + + let profile = core_load_client_config_profile(options, env_vars.as_ref()) + .map_err(|e| error!("EnvConfig error: {}", e))?; + + profile_to_hash(ruby, &profile) +} + +// load_client_config(path: String|nil, data: String|nil, disable_file: bool, config_file_strict: bool, env_vars: Hash|nil) +fn load_client_config(args: &[magnus::Value]) -> Result { + let ruby = Ruby::get().expect("Not in Ruby thread"); + let args = scan_args::scan_args::< + (Option, Option, bool, bool), + (Option>,), + (), + (), + (), + (), + >(args)?; + let (path, data, disable_file, config_file_strict) = args.required; + let (env_vars,) = args.optional; + + let config_source = match (path, data) { + (Some(p), None) => Some(DataSource::Path(p)), + (None, Some(d)) => { + let bytes = unsafe { d.as_slice().to_vec() }; + Some(DataSource::Data(bytes)) + } + (None, None) => None, + (Some(_), Some(_)) => { + return Err(error!( + "Cannot specify both path and data for config source" + )); + } + }; + + load_client_config_inner( + &ruby, + config_source, + config_file_strict, + disable_file, + env_vars, + ) +} + +// load_client_connect_config(profile: String|nil, path: String|nil, data: String|nil, disable_file: bool, disable_env: bool, config_file_strict: bool, env_vars: Hash|nil) +fn load_client_connect_config(args: &[magnus::Value]) -> Result { + let ruby = Ruby::get().expect("Not in Ruby thread"); + let args = scan_args::scan_args::< + ( + Option, + Option, + Option, + bool, + bool, + bool, + ), + (Option>,), + (), + (), + (), + (), + >(args)?; + let (profile, path, data, disable_file, disable_env, config_file_strict) = args.required; + let (env_vars,) = args.optional; + + let config_source = match (path, data) { + (Some(p), None) => Some(DataSource::Path(p)), + (None, Some(d)) => { + let bytes = unsafe { d.as_slice().to_vec() }; + Some(DataSource::Data(bytes)) + } + (None, None) => None, + (Some(_), Some(_)) => { + return Err(error!( + "Cannot specify both path and data for config source" + )); + } + }; + + load_client_connect_config_inner( + &ruby, + config_source, + profile, + disable_file, + disable_env, + config_file_strict, + env_vars, + ) +} diff --git a/temporalio/ext/src/lib.rs b/temporalio/ext/src/lib.rs index 9a7f692c..9979acc1 100644 --- a/temporalio/ext/src/lib.rs +++ b/temporalio/ext/src/lib.rs @@ -2,6 +2,7 @@ use magnus::{Error, ExceptionClass, RModule, Ruby, prelude::*, value::Lazy}; mod client; mod client_rpc_generated; +mod envconfig; mod metric; mod runtime; mod testing; @@ -50,6 +51,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> { Lazy::force(&ROOT_ERR, ruby); client::init(ruby)?; + envconfig::init(ruby)?; metric::init(ruby)?; runtime::init(ruby)?; testing::init(ruby)?; diff --git a/temporalio/lib/temporalio/cancellation.rb b/temporalio/lib/temporalio/cancellation.rb index 9b33b204..f1e12a46 100644 --- a/temporalio/lib/temporalio/cancellation.rb +++ b/temporalio/lib/temporalio/cancellation.rb @@ -167,8 +167,8 @@ def prepare_cancel(reason:) to_return.values end - def canceled_mutex_synchronize(&) - Workflow::Unsafe.illegal_call_tracing_disabled { @canceled_mutex.synchronize(&) } + def canceled_mutex_synchronize(&block) + Workflow::Unsafe.illegal_call_tracing_disabled { @canceled_mutex.synchronize(&block) } end end end diff --git a/temporalio/lib/temporalio/env_config.rb b/temporalio/lib/temporalio/env_config.rb new file mode 100644 index 00000000..6ee88393 --- /dev/null +++ b/temporalio/lib/temporalio/env_config.rb @@ -0,0 +1,339 @@ +# frozen_string_literal: true + +require 'pathname' +require 'temporalio/internal/bridge' + +module Temporalio + # Environment and file-based configuration for Temporal clients + # + # WARNING: Experimental API. + module EnvConfig + # This module provides utilities to load Temporal client configuration from TOML files + # and environment variables. + + # TLS configuration as specified as part of client configuration + # + # @!attribute [r] disabled + # @return [Boolean, nil] If true, TLS is explicitly disabled; if nil, not specified + # @!attribute [r] server_name + # @return [String, nil] SNI override + # @!attribute [r] server_root_ca_cert + # @return [Pathname, String, nil] Server CA certificate source + # @!attribute [r] client_cert + # @return [Pathname, String, nil] Client certificate source + # @!attribute [r] client_private_key + # @return [Pathname, String, nil] Client key source + ClientConfigTLS = Data.define(:disabled, :server_name, :server_root_ca_cert, :client_cert, :client_private_key) + + # TLS configuration for Temporal client connections. + # + # This class provides methods for creating, serializing, and converting + # TLS configuration objects used by Temporal clients. + class ClientConfigTLS + # Create a ClientConfigTLS from a hash + # @param hash [Hash, nil] Hash representation + # @return [ClientConfigTLS, nil] The TLS configuration or nil if hash is nil/empty + def self.from_h(hash) + return nil if hash.nil? || hash.empty? + + new( + disabled: hash[:disabled], + server_name: hash[:server_name], + server_root_ca_cert: hash_to_source(hash[:server_ca_cert]), + client_cert: hash_to_source(hash[:client_cert]), + client_private_key: hash_to_source(hash[:client_key]) + ) + end + + # Set default values + def initialize(disabled: nil, server_name: nil, server_root_ca_cert: nil, client_cert: nil, + client_private_key: nil) + super + end + + # Convert to a hash that can be used for TOML serialization + # @return [Hash] Dictionary representation + def to_h + { + disabled:, + server_name:, + server_ca_cert: server_root_ca_cert ? source_to_hash(server_root_ca_cert) : nil, + client_cert: client_cert ? source_to_hash(client_cert) : nil, + client_key: client_private_key ? source_to_hash(client_private_key) : nil + }.compact + end + + # Create a TLS configuration for use with connections + # @return [Connection::TLSOptions, false] TLS options or false if disabled + def to_client_tls_options + return false if disabled + + Client::Connection::TLSOptions.new( + domain: server_name, + server_root_ca_cert: read_source(server_root_ca_cert), + client_cert: read_source(client_cert), + client_private_key: read_source(client_private_key) + ) + end + + private + + class << self + private + + # Convert hash to source object (Pathname or String) + def hash_to_source(hash) + return nil if hash.nil? + + # Always expect a hash with path or data + if hash[:path] + Pathname.new(hash[:path]) + elsif hash[:data] + hash[:data] + end + end + end + + def source_to_hash(source) + case source + when Pathname + { path: source.to_s } + when String + # String is always treated as data content + { data: source } + when nil + nil + else + raise TypeError, "Source must be Pathname, String, or nil, got #{source.class}" + end + end + + def read_source(source) + case source + when Pathname + File.read(source.to_s) + when String + # String is always treated as raw data content + source + when nil + nil + else + raise TypeError, "Source must be Pathname, String, or nil, got #{source.class}" + end + end + end + + # Represents a client configuration profile. + # + # This class holds the configuration as loaded from a file or environment. + # See #to_client_connect_options to transform the profile to a connect config hash. + # + # @!attribute [r] address + # @return [String, nil] Client address + # @!attribute [r] namespace + # @return [String, nil] Client namespace + # @!attribute [r] api_key + # @return [String, nil] Client API key + # @!attribute [r] tls + # @return [ClientConfigTLS, nil] TLS configuration + # @!attribute [r] grpc_meta + # @return [Hash] gRPC metadata + ClientConfigProfile = Data.define(:address, :namespace, :api_key, :tls, :grpc_meta) + + # A client configuration profile loaded from environment and files. + # + # This class represents a complete client configuration profile that can be + # loaded from TOML files and environment variables, and converted to client + # connection options. + class ClientConfigProfile + # Create a ClientConfigProfile from a hash + # @param hash [Hash] Hash representation + # @return [ClientConfigProfile] The client profile + def self.from_h(hash) + new( + address: hash[:address], + namespace: hash[:namespace], + api_key: hash[:api_key], + tls: ClientConfigTLS.from_h(hash[:tls]), + grpc_meta: hash[:grpc_meta] || {} + ) + end + + # Load a single client profile from given sources, applying env overrides. + # + # @param profile [String, nil] Profile to load from the config + # @param config_source [Pathname, String, nil] Configuration source - + # Pathname for file path, String for TOML content + # @param disable_file [Boolean] If true, file loading is disabled + # @param disable_env [Boolean] If true, environment variable loading and overriding is disabled + # @param config_file_strict [Boolean] If true, will error on unrecognized keys + # @param override_env_vars [Hash, nil] Environment variables to use for loading and overrides + # @return [ClientConfigProfile] The client configuration profile + def self.load( + profile: nil, + config_source: nil, + disable_file: false, + disable_env: false, + config_file_strict: false, + override_env_vars: nil + ) + path, data = EnvConfig._source_to_path_and_data(config_source) + + raw_profile = Internal::Bridge::EnvConfig.load_client_connect_config( + profile, + path, + data, + disable_file, + disable_env, + config_file_strict, + override_env_vars || {} + ) + + from_h(raw_profile) + end + + # Create a ClientConfigProfile instance with defaults + def initialize(address: nil, namespace: nil, api_key: nil, tls: nil, grpc_meta: {}) + super + end + + # Convert to a hash that can be used for TOML serialization + # @return [Hash] Dictionary representation + def to_h + { + address: address, + namespace: namespace, + api_key: api_key, + tls: tls&.to_h&.then { |tls_hash| tls_hash.empty? ? nil : tls_hash }, # steep:ignore + grpc_meta: grpc_meta && grpc_meta.empty? ? nil : grpc_meta + }.compact + end + + # Create a client connect config from this profile + # @return [Array] Tuple of [positional_args, keyword_args] that can be splatted to Client.connect + def to_client_connect_options + positional_args = [address, namespace].compact + keyword_args = { + api_key: api_key, + tls: tls&.to_client_tls_options, + rpc_metadata: (grpc_meta if grpc_meta && !grpc_meta.empty?) + }.compact + + [positional_args, keyword_args] + end + end + + # Client configuration loaded from TOML and environment variables. + # + # This contains a mapping of profile names to client profiles. + # + # @!attribute [r] profiles + # @return [Hash] Map of profile name to its corresponding ClientConfigProfile + ClientConfig = Data.define(:profiles) + + # Container for multiple client configuration profiles. + # + # This class holds a collection of named client profiles loaded from + # configuration sources and provides methods for profile management + # and client connection configuration. + class ClientConfig + # Create a ClientConfig from a hash + # @param hash [Hash] Hash representation + # @return [ClientConfig] The client configuration + def self.from_h(hash) + profiles = hash.transform_values do |profile_hash| + ClientConfigProfile.from_h(profile_hash) + end + new(profiles: profiles) + end + + # Load all client profiles from given sources. + # + # This does not apply environment variable overrides to the profiles, it + # only uses an environment variable to find the default config file path + # (TEMPORAL_CONFIG_FILE). + # + # @param config_source [Pathname, String, nil] Configuration source + # @param disable_file [Boolean] If true, file loading is disabled + # @param config_file_strict [Boolean] If true, will error on unrecognized keys + # @param override_env_vars [Hash, nil] Environment variables to use + # @return [ClientConfig] The client configuration + def self.load( + config_source: nil, + disable_file: false, + config_file_strict: false, + override_env_vars: nil + ) + path, data = EnvConfig._source_to_path_and_data(config_source) + + loaded_profiles = Internal::Bridge::EnvConfig.load_client_config( + path, + data, + disable_file, + config_file_strict, + override_env_vars || {} + ) + + from_h(loaded_profiles) + end + + # Load a single client profile and convert to connect config + # + # This is a convenience function that combines loading a profile and + # converting it to a connect config hash. + # + # @param profile [String, nil] The profile to load from the config + # @param config_source [Pathname, String, nil] Configuration source + # @param disable_file [Boolean] If true, file loading is disabled + # @param disable_env [Boolean] If true, environment variable loading and overriding is disabled + # @param config_file_strict [Boolean] If true, will error on unrecognized keys + # @param override_env_vars [Hash, nil] Environment variables to use for loading and overrides + # @return [Array] Tuple of [positional_args, keyword_args] that can be splatted to Client.connect + def self.load_client_connect_options( + profile: nil, + config_source: nil, + disable_file: false, + disable_env: false, + config_file_strict: false, + override_env_vars: nil + ) + prof = ClientConfigProfile.load( + profile: profile, + config_source: config_source, + disable_file: disable_file, + disable_env: disable_env, + config_file_strict: config_file_strict, + override_env_vars: override_env_vars + ) + prof.to_client_connect_options + end + + # Create a ClientConfig instance with defaults + def initialize(profiles: {}) + super + end + + # Convert to a hash that can be used for TOML serialization + # @return [Hash] Dictionary representation + def to_h + profiles.transform_values(&:to_h) + end + end + + # @param source [Pathname, String, nil] Configuration source + # @return [Array] Tuple of [path, data] + # @!visibility private + def self._source_to_path_and_data(source) + case source + when Pathname + [source.to_s, nil] + when String + [nil, source] + when nil + [nil, nil] + else + raise TypeError, "Must be Pathname, String, or nil, got #{source.class}" + end + end + end +end diff --git a/temporalio/lib/temporalio/internal/worker/workflow_instance/outbound_implementation.rb b/temporalio/lib/temporalio/internal/worker/workflow_instance/outbound_implementation.rb index a1bf206e..1a23daa9 100644 --- a/temporalio/lib/temporalio/internal/worker/workflow_instance/outbound_implementation.rb +++ b/temporalio/lib/temporalio/internal/worker/workflow_instance/outbound_implementation.rb @@ -121,7 +121,7 @@ def execute_local_activity(input) end end - def execute_activity_with_local_backoffs(local:, cancellation:, result_hint:, &) + def execute_activity_with_local_backoffs(local:, cancellation:, result_hint:, &block) # We do not even want to schedule if the cancellation is already cancelled. We choose to use canceled # failure instead of wrapping in activity failure which is similar to what other SDKs do, with the accepted # tradeoff that it makes rescue more difficult (hence the presence of Error.canceled? helper). @@ -130,7 +130,7 @@ def execute_activity_with_local_backoffs(local:, cancellation:, result_hint:, &) # This has to be done in a loop for local activity backoff last_local_backoff = nil loop do - result = execute_activity_once(local:, cancellation:, last_local_backoff:, result_hint:, &) + result = execute_activity_once(local:, cancellation:, last_local_backoff:, result_hint:, &block) return result unless result.is_a?(Bridge::Api::ActivityResult::DoBackoff) # @type var result: untyped @@ -142,9 +142,9 @@ def execute_activity_with_local_backoffs(local:, cancellation:, result_hint:, &) end # If this doesn't raise, it returns success | DoBackoff - def execute_activity_once(local:, cancellation:, last_local_backoff:, result_hint:, &) + def execute_activity_once(local:, cancellation:, last_local_backoff:, result_hint:, &block) # Add to pending activities (removed by the resolver) - seq = yield last_local_backoff + seq = block.call(last_local_backoff) @instance.pending_activities[seq] = Fiber.current # Add cancellation hook diff --git a/temporalio/sig/temporalio/envconfig.rbs b/temporalio/sig/temporalio/envconfig.rbs new file mode 100644 index 00000000..99b0380f --- /dev/null +++ b/temporalio/sig/temporalio/envconfig.rbs @@ -0,0 +1,87 @@ +module Temporalio + module EnvConfig + def self._source_to_path_and_data: (untyped source) -> [String?, String?] + + class ClientConfigTLS + attr_reader disabled: bool? + attr_reader server_name: String? + attr_reader server_root_ca_cert: (Pathname | String)? + attr_reader client_cert: (Pathname | String)? + attr_reader client_private_key: (Pathname | String)? + + def self.from_h: (Hash[untyped, untyped]? hash) -> ClientConfigTLS? + def self.hash_to_source: (Hash[untyped, untyped]? hash) -> (Pathname | String)? + + def initialize: ( + ?disabled: bool?, + ?server_name: String?, + ?server_root_ca_cert: (Pathname | String)?, + ?client_cert: (Pathname | String)?, + ?client_private_key: (Pathname | String)? + ) -> void + + def to_h: -> Hash[Symbol, untyped] + def to_client_tls_options: -> (untyped | false) + + private + + def source_to_hash: (untyped source) -> Hash[Symbol, String]? + def read_source: (untyped source) -> String? + end + + class ClientConfigProfile + attr_reader address: String? + attr_reader namespace: String? + attr_reader api_key: String? + attr_reader tls: ClientConfigTLS? + attr_reader grpc_meta: Hash[untyped, untyped] + + def self.from_h: (Hash[untyped, untyped] hash) -> ClientConfigProfile + + def self.load: ( + ?profile: String?, + ?config_source: (Pathname | String)?, + ?disable_file: bool, + ?disable_env: bool, + ?config_file_strict: bool, + ?override_env_vars: Hash[String, String]? + ) -> ClientConfigProfile + + def initialize: ( + ?address: String?, + ?namespace: String?, + ?api_key: String?, + ?tls: ClientConfigTLS?, + ?grpc_meta: Hash[untyped, untyped] + ) -> void + + def to_h: -> Hash[Symbol, untyped] + def to_client_connect_options: -> [Array[untyped], Hash[Symbol, untyped]] + end + + class ClientConfig + attr_reader profiles: Hash[String, ClientConfigProfile] + + def self.from_h: (Hash[untyped, untyped] hash) -> ClientConfig + + def self.load: ( + ?config_source: (Pathname | String)?, + ?disable_file: bool, + ?config_file_strict: bool, + ?override_env_vars: Hash[String, String]? + ) -> ClientConfig + + def self.load_client_connect_options: ( + ?profile: String?, + ?config_source: (Pathname | String)?, + ?disable_file: bool, + ?disable_env: bool, + ?config_file_strict: bool, + ?override_env_vars: Hash[String, String]? + ) -> [Array[untyped], Hash[Symbol, untyped]] + + def initialize: (?profiles: Hash[String, ClientConfigProfile]) -> void + def to_h: -> Hash[String, Hash[Symbol, untyped]] + end + end +end \ No newline at end of file diff --git a/temporalio/test/envconfig_test.rb b/temporalio/test/envconfig_test.rb new file mode 100644 index 00000000..1c84defe --- /dev/null +++ b/temporalio/test/envconfig_test.rb @@ -0,0 +1,912 @@ +# frozen_string_literal: true + +require 'fileutils' +require 'pathname' +require 'temporalio/client' +require 'temporalio/env_config' +require 'tmpdir' +require_relative 'test' + +class EnvConfigTest < Test + # A base TOML config with a default and a custom profile + TOML_CONFIG_BASE = <<~TOML + [profile.default] + address = "default-address" + namespace = "default-namespace" + + [profile.custom] + address = "custom-address" + namespace = "custom-namespace" + api_key = "custom-api-key" + [profile.custom.tls] + server_name = "custom-server-name" + [profile.custom.grpc_meta] + custom-header = "custom-value" + TOML + + # A TOML config with an unrecognized key for strict testing + TOML_CONFIG_STRICT_FAIL = <<~TOML + [profile.default] + address = "default-address" + unrecognized_field = "should-fail" + TOML + + # Malformed TOML for error testing + TOML_CONFIG_MALFORMED = 'this is not valid toml' + + # A TOML config for testing detailed TLS options + TOML_CONFIG_TLS_DETAILED = <<~TOML + [profile.tls_disabled] + address = "localhost:1234" + [profile.tls_disabled.tls] + disabled = true + server_name = "should-be-ignored" + + [profile.tls_with_certs] + address = "localhost:5678" + [profile.tls_with_certs.tls] + server_name = "custom-server" + server_ca_cert_data = "ca-pem-data" + client_cert_data = "client-crt-data" + client_key_data = "client-key-data" + TOML + + # TOML config for metadata normalization testing + TOML_CONFIG_GRPC_META = <<~TOML + [profile.default] + address = "localhost:7233" + namespace = "default" + + [profile.default.grpc_meta] + "Custom-Header" = "custom-value" + "ANOTHER_HEADER_KEY" = "another-value" + "mixed_Case-header" = "mixed-value" + TOML + + # ============================================================================= + # PROFILE LOADING TESTS (6 tests) + # ============================================================================= + + def test_load_profile_from_file_default + with_temp_config_file(TOML_CONFIG_BASE) do |config_file| + profile = Temporalio::EnvConfig::ClientConfigProfile.load(config_source: Pathname.new(config_file)) + assert_equal 'default-address', profile.address + assert_equal 'default-namespace', profile.namespace + assert_nil profile.tls + refute_includes profile.grpc_meta, 'custom-header' + + args, kwargs = profile.to_client_connect_options + assert_equal 'default-address', args[0] + assert_equal 'default-namespace', args[1] + assert_nil kwargs[:tls] + rpc_meta = kwargs[:rpc_metadata] + if rpc_meta.nil? + assert_nil rpc_meta + else + refute_includes rpc_meta, 'custom-header' + end + end + end + + def test_load_profile_from_file_custom + with_temp_config_file(TOML_CONFIG_BASE) do |config_file| + profile = Temporalio::EnvConfig::ClientConfigProfile.load(config_source: Pathname.new(config_file), + profile: 'custom') + assert_equal 'custom-address', profile.address + assert_equal 'custom-namespace', profile.namespace + refute_nil profile.tls + assert_equal 'custom-server-name', profile.tls.server_name # steep:ignore + assert_equal 'custom-value', profile.grpc_meta['custom-header'] + + args, kwargs = profile.to_client_connect_options + assert_equal 'custom-address', args[0] + assert_equal 'custom-namespace', args[1] + tls_config = kwargs[:tls] + assert_instance_of Temporalio::Client::Connection::TLSOptions, tls_config + assert_equal 'custom-server-name', tls_config.domain + rpc_metadata = kwargs[:rpc_metadata] + refute_nil rpc_metadata + assert_equal 'custom-value', rpc_metadata['custom-header'] + end + end + + def test_load_profile_from_data_default + profile = Temporalio::EnvConfig::ClientConfigProfile.load(config_source: TOML_CONFIG_BASE) + assert_equal 'default-address', profile.address + assert_equal 'default-namespace', profile.namespace + assert_nil profile.tls + + args, kwargs = profile.to_client_connect_options + assert_equal 'default-address', args[0] + assert_equal 'default-namespace', args[1] + assert_nil kwargs[:tls] + end + + def test_load_profile_from_data_custom + profile = Temporalio::EnvConfig::ClientConfigProfile.load(config_source: TOML_CONFIG_BASE, profile: 'custom') + assert_equal 'custom-address', profile.address + assert_equal 'custom-namespace', profile.namespace + refute_nil profile.tls + assert_equal 'custom-server-name', profile.tls.server_name # steep:ignore + assert_equal 'custom-value', profile.grpc_meta['custom-header'] + + args, kwargs = profile.to_client_connect_options + assert_equal 'custom-address', args[0] + assert_equal 'custom-namespace', args[1] + tls_config = kwargs[:tls] + assert_instance_of Temporalio::Client::Connection::TLSOptions, tls_config + assert_equal 'custom-server-name', tls_config.domain + rpc_metadata = kwargs[:rpc_metadata] + refute_nil rpc_metadata + assert_equal 'custom-value', rpc_metadata['custom-header'] + end + + def test_load_profile_from_data_env_overrides + env = { + 'TEMPORAL_ADDRESS' => 'env-address', + 'TEMPORAL_NAMESPACE' => 'env-namespace' + } + profile = Temporalio::EnvConfig::ClientConfigProfile.load( + config_source: TOML_CONFIG_BASE, profile: 'custom', override_env_vars: env + ) + assert_equal 'env-address', profile.address + assert_equal 'env-namespace', profile.namespace + + args, = profile.to_client_connect_options + assert_equal 'env-address', args[0] + assert_equal 'env-namespace', args[1] + end + + def test_load_profile_env_overrides + with_temp_config_file(TOML_CONFIG_BASE) do |config_file| + env = { + 'TEMPORAL_ADDRESS' => 'env-address', + 'TEMPORAL_NAMESPACE' => 'env-namespace', + 'TEMPORAL_API_KEY' => 'env-api-key', + 'TEMPORAL_TLS_SERVER_NAME' => 'env-server-name' + } + profile = Temporalio::EnvConfig::ClientConfigProfile.load( + config_source: Pathname.new(config_file), profile: 'custom', override_env_vars: env + ) + assert_equal 'env-address', profile.address + assert_equal 'env-namespace', profile.namespace + assert_equal 'env-api-key', profile.api_key + refute_nil profile.tls + assert_equal 'env-server-name', profile.tls.server_name # steep:ignore + + args, kwargs = profile.to_client_connect_options + assert_equal 'env-address', args[0] + assert_equal 'env-namespace', args[1] + assert_equal 'env-api-key', kwargs[:api_key] + tls_config = kwargs[:tls] + assert_instance_of Temporalio::Client::Connection::TLSOptions, tls_config + assert_equal 'env-server-name', tls_config.domain + end + end + + # ============================================================================= + # ENVIRONMENT VARIABLES TESTS (4 tests) + # ============================================================================= + + def test_load_profile_grpc_meta_env_overrides + with_temp_config_file(TOML_CONFIG_BASE) do |config_file| + env = { + # This should override the value in the file + 'TEMPORAL_GRPC_META_CUSTOM_HEADER' => 'env-value', + # This should add a new header + 'TEMPORAL_GRPC_META_ANOTHER_HEADER' => 'another-value' + } + profile = Temporalio::EnvConfig::ClientConfigProfile.load( + config_source: Pathname.new(config_file), profile: 'custom', override_env_vars: env + ) + assert_equal 'env-value', profile.grpc_meta['custom-header'] + assert_equal 'another-value', profile.grpc_meta['another-header'] + + _, kwargs = profile.to_client_connect_options + rpc_metadata = kwargs[:rpc_metadata] + refute_nil rpc_metadata + assert_equal 'env-value', rpc_metadata['custom-header'] + assert_equal 'another-value', rpc_metadata['another-header'] + end + end + + def test_grpc_metadata_normalization_from_toml + profile = Temporalio::EnvConfig::ClientConfigProfile.load(config_source: TOML_CONFIG_GRPC_META) + + # Keys should be normalized: uppercase -> lowercase, underscores -> hyphens + assert_equal 'custom-value', profile.grpc_meta['custom-header'] + assert_equal 'another-value', profile.grpc_meta['another-header-key'] + assert_equal 'mixed-value', profile.grpc_meta['mixed-case-header'] + + # Original case variations should not exist + refute_includes profile.grpc_meta, 'Custom-Header' + refute_includes profile.grpc_meta, 'ANOTHER_HEADER_KEY' + refute_includes profile.grpc_meta, 'mixed_Case-header' + + _, kwargs = profile.to_client_connect_options + rpc_metadata = kwargs[:rpc_metadata] + refute_nil rpc_metadata + assert_equal 'custom-value', rpc_metadata['custom-header'] + assert_equal 'another-value', rpc_metadata['another-header-key'] + end + + def test_grpc_metadata_deletion_via_empty_env_value + with_temp_config_file(TOML_CONFIG_BASE) do |config_file| + env = { + # Empty value should remove the header + 'TEMPORAL_GRPC_META_CUSTOM_HEADER' => '', + # Non-empty value should set the header + 'TEMPORAL_GRPC_META_NEW_HEADER' => 'new-value' + } + profile = Temporalio::EnvConfig::ClientConfigProfile.load( + config_source: Pathname.new(config_file), profile: 'custom', override_env_vars: env + ) + + # custom-header should be removed by empty env value + refute_includes profile.grpc_meta, 'custom-header' + # new-header should be added + assert_equal 'new-value', profile.grpc_meta['new-header'] + + _, kwargs = profile.to_client_connect_options + rpc_metadata = kwargs[:rpc_metadata] + if rpc_metadata && !rpc_metadata.empty? + refute_includes rpc_metadata, 'custom-header' + assert_equal 'new-value', rpc_metadata['new-header'] + end + end + end + + def test_load_profile_disable_env + with_temp_config_file(TOML_CONFIG_BASE) do |config_file| + env = { 'TEMPORAL_ADDRESS' => 'env-address' } + profile = Temporalio::EnvConfig::ClientConfigProfile.load( + config_source: Pathname.new(config_file), override_env_vars: env, disable_env: true + ) + assert_equal 'default-address', profile.address + + args, = profile.to_client_connect_options + assert_equal 'default-address', args[0] + end + end + + # ============================================================================= + # CONTROL FLAGS TESTS (3 tests) + # ============================================================================= + + def test_load_profile_disable_file + env = { 'TEMPORAL_ADDRESS' => 'env-address' } + profile = Temporalio::EnvConfig::ClientConfigProfile.load(disable_file: true, override_env_vars: env) + assert_equal 'env-address', profile.address + + args, = profile.to_client_connect_options + assert_equal 'env-address', args[0] + end + + def test_load_profiles_no_env_override + with_temp_config_file(TOML_CONFIG_BASE) do |config_file| + env = { + 'TEMPORAL_CONFIG_FILE' => config_file, + 'TEMPORAL_ADDRESS' => 'env-address' # This should be ignored for profiles loading + } + client_config = Temporalio::EnvConfig::ClientConfig.load(override_env_vars: env) + args, = client_config.profiles['default'].to_client_connect_options + assert_equal 'default-address', args[0] + end + end + + def test_disables_raise_error + assert_raises(Temporalio::Internal::Bridge::Error) do + Temporalio::EnvConfig::ClientConfigProfile.load(disable_file: true, disable_env: true) + end + end + + # ============================================================================= + # CONFIG DISCOVERY TESTS (6 tests) + # ============================================================================= + + def test_load_profiles_from_file_all + with_temp_config_file(TOML_CONFIG_BASE) do |config_file| + client_config = Temporalio::EnvConfig::ClientConfig.load(config_source: Pathname.new(config_file)) + assert_equal 2, client_config.profiles.size + assert_includes client_config.profiles, 'default' + assert_includes client_config.profiles, 'custom' + # Check that we can convert to a connect config + args, = client_config.profiles['default'].to_client_connect_options + assert_equal 'default-address', args[0] + end + end + + def test_load_profiles_from_data_all + client_config = Temporalio::EnvConfig::ClientConfig.load(config_source: TOML_CONFIG_BASE) + assert_equal 2, client_config.profiles.size + args, = client_config.profiles['custom'].to_client_connect_options + assert_equal 'custom-address', args[0] + end + + def test_load_profiles_no_config_file + client_config = Temporalio::EnvConfig::ClientConfig.load(override_env_vars: {}) + assert_empty client_config.profiles + end + + def test_load_profiles_discovery + with_temp_config_file(TOML_CONFIG_BASE) do |config_file| + env = { 'TEMPORAL_CONFIG_FILE' => config_file } + client_config = Temporalio::EnvConfig::ClientConfig.load(override_env_vars: env) + assert_includes client_config.profiles, 'default' + end + end + + def test_load_profiles_disable_file + # With no env vars, should be empty + client_config = Temporalio::EnvConfig::ClientConfig.load(disable_file: true, override_env_vars: {}) + assert_empty client_config.profiles + end + + def test_default_profile_not_found_returns_empty_profile + toml = <<~TOML + [profile.existing] + address = "my-address" + TOML + profile = Temporalio::EnvConfig::ClientConfigProfile.load(config_source: toml) + assert_nil profile.address + assert_nil profile.namespace + assert_nil profile.api_key + assert_empty profile.grpc_meta + assert_nil profile.tls + end + + # ============================================================================= + # TLS CONFIGURATION TESTS (7 tests) + # ============================================================================= + + def test_load_profile_api_key_enables_tls + config_toml = "[profile.default]\naddress = 'some-host:1234'\napi_key = 'my-key'" + profile = Temporalio::EnvConfig::ClientConfigProfile.load(config_source: config_toml) + assert_equal 'my-key', profile.api_key + refute_nil profile.tls + + _, kwargs = profile.to_client_connect_options + refute_nil kwargs[:tls] + assert_equal 'my-key', kwargs[:api_key] + end + + def test_load_profile_tls_options + # Test with TLS disabled + profile_disabled = Temporalio::EnvConfig::ClientConfigProfile.load( + config_source: TOML_CONFIG_TLS_DETAILED, profile: 'tls_disabled' + ) + refute_nil profile_disabled.tls + assert profile_disabled.tls.disabled # steep:ignore + + _, kwargs_disabled = profile_disabled.to_client_connect_options + assert_equal false, kwargs_disabled[:tls] + + # Test with TLS certs + profile_certs = Temporalio::EnvConfig::ClientConfigProfile.load( + config_source: TOML_CONFIG_TLS_DETAILED, profile: 'tls_with_certs' + ) + refute_nil profile_certs.tls + assert_equal 'custom-server', profile_certs.tls.server_name # steep:ignore + refute_nil profile_certs.tls.server_root_ca_cert # steep:ignore + assert_equal 'ca-pem-data', profile_certs.tls.server_root_ca_cert # steep:ignore + refute_nil profile_certs.tls.client_cert # steep:ignore + assert_equal 'client-crt-data', profile_certs.tls.client_cert # steep:ignore + refute_nil profile_certs.tls.client_private_key # steep:ignore + assert_equal 'client-key-data', profile_certs.tls.client_private_key # steep:ignore + + _, kwargs_certs = profile_certs.to_client_connect_options + tls_config_certs = kwargs_certs[:tls] + assert_instance_of Temporalio::Client::Connection::TLSOptions, tls_config_certs + assert_equal 'custom-server', tls_config_certs.domain + assert_equal 'ca-pem-data', tls_config_certs.server_root_ca_cert + assert_equal 'client-crt-data', tls_config_certs.client_cert + assert_equal 'client-key-data', tls_config_certs.client_private_key + end + + def test_load_profile_tls_from_paths + Dir.mktmpdir do |tmpdir| # steep:ignore + # Create dummy cert files + ca_pem_path = File.join(tmpdir, 'ca.pem') + client_crt_path = File.join(tmpdir, 'client.crt') + client_key_path = File.join(tmpdir, 'client.key') + + File.write(ca_pem_path, 'ca-pem-data') + File.write(client_crt_path, 'client-crt-data') + File.write(client_key_path, 'client-key-data') + + toml_config = <<~TOML + [profile.default] + address = "localhost:5678" + [profile.default.tls] + server_name = "custom-server" + server_ca_cert_path = "#{ca_pem_path}" + client_cert_path = "#{client_crt_path}" + client_key_path = "#{client_key_path}" + TOML + + profile = Temporalio::EnvConfig::ClientConfigProfile.load(config_source: toml_config) + refute_nil profile.tls + assert_equal 'custom-server', profile.tls.server_name # steep:ignore + refute_nil profile.tls.server_root_ca_cert # steep:ignore + assert_equal Pathname.new(ca_pem_path), profile.tls.server_root_ca_cert # steep:ignore + refute_nil profile.tls.client_cert # steep:ignore + assert_equal Pathname.new(client_crt_path), profile.tls.client_cert # steep:ignore + refute_nil profile.tls.client_private_key # steep:ignore + assert_equal Pathname.new(client_key_path), profile.tls.client_private_key # steep:ignore + + _, kwargs = profile.to_client_connect_options + tls_config = kwargs[:tls] + assert_instance_of Temporalio::Client::Connection::TLSOptions, tls_config + assert_equal 'custom-server', tls_config.domain + assert_equal 'ca-pem-data', tls_config.server_root_ca_cert + assert_equal 'client-crt-data', tls_config.client_cert + assert_equal 'client-key-data', tls_config.client_private_key + end + end + + def test_load_profile_conflicting_cert_source_fails + toml_config = <<~TOML + [profile.default] + address = "localhost:5678" + [profile.default.tls] + client_cert_path = "/path/to/cert" + client_cert_data = "cert-data" + TOML + assert_raises(Temporalio::Internal::Bridge::Error) do + Temporalio::EnvConfig::ClientConfigProfile.load(config_source: toml_config) + end + end + + def test_tls_conflict_across_sources_path_in_toml_data_in_env + toml_config = <<~TOML + [profile.default] + address = "localhost:7233" + [profile.default.tls] + client_cert_path = "/path/to/cert" + TOML + + env = { + 'TEMPORAL_TLS_CLIENT_CERT_DATA' => 'cert-data-from-env' + } + + assert_raises(Temporalio::Internal::Bridge::Error) do + Temporalio::EnvConfig::ClientConfigProfile.load( + config_source: toml_config, + override_env_vars: env + ) + end + end + + def test_tls_conflict_across_sources_data_in_toml_path_in_env + toml_config = <<~TOML + [profile.default] + address = "localhost:7233" + [profile.default.tls] + client_cert_data = "cert-data-from-toml" + TOML + + env = { + 'TEMPORAL_TLS_CLIENT_CERT_PATH' => '/path/from/env' + } + + assert_raises(Temporalio::Internal::Bridge::Error) do + Temporalio::EnvConfig::ClientConfigProfile.load( + config_source: toml_config, + override_env_vars: env + ) + end + end + + # ============================================================================= + # ERROR HANDLING TESTS (4 tests) + # ============================================================================= + + def test_load_profile_not_found + with_temp_config_file(TOML_CONFIG_BASE) do |config_file| + assert_raises(Temporalio::Internal::Bridge::Error) do + Temporalio::EnvConfig::ClientConfigProfile.load(config_source: Pathname.new(config_file), + profile: 'nonexistent') + end + end + end + + def test_load_profiles_strict_mode_fail + with_temp_config_file(TOML_CONFIG_STRICT_FAIL) do |config_file| + assert_raises(Temporalio::Internal::Bridge::Error) do + Temporalio::EnvConfig::ClientConfig.load(config_source: Pathname.new(config_file), config_file_strict: true) + end + end + end + + def test_load_profile_strict_mode_fail + with_temp_config_file(TOML_CONFIG_STRICT_FAIL) do |config_file| + assert_raises(Temporalio::Internal::Bridge::Error) do + Temporalio::EnvConfig::ClientConfigProfile.load(config_source: Pathname.new(config_file), + config_file_strict: true) + end + end + end + + def test_load_profiles_from_data_malformed + assert_raises(Temporalio::Internal::Bridge::Error) do + Temporalio::EnvConfig::ClientConfig.load(config_source: TOML_CONFIG_MALFORMED) + end + end + + # ============================================================================= + # SERIALIZATION TESTS (3 tests) + # ============================================================================= + + def test_client_config_profile_to_from_dict + # Profile with all fields + profile = Temporalio::EnvConfig::ClientConfigProfile.new( + address: 'some-address', + namespace: 'some-namespace', + api_key: 'some-api-key', + tls: Temporalio::EnvConfig::ClientConfigTLS.new( + disabled: false, + server_name: 'some-server-name', + server_root_ca_cert: 'ca-cert-data', + client_cert: Pathname.new('/path/to/client.crt'), + client_private_key: 'client-key-data' + ), + grpc_meta: { 'some-header' => 'some-value' } + ) + + profile_hash = profile.to_h + + # Check hash representation. disabled=false is now included since it was explicitly set. + expected_hash = { + address: 'some-address', + namespace: 'some-namespace', + api_key: 'some-api-key', + tls: { + disabled: false, + server_name: 'some-server-name', + server_ca_cert: { data: 'ca-cert-data' }, + client_cert: { path: '/path/to/client.crt' }, + client_key: { data: 'client-key-data' } + }, + grpc_meta: { 'some-header' => 'some-value' } + } + assert_equal expected_hash, profile_hash + + # Convert back to profile + new_profile = Temporalio::EnvConfig::ClientConfigProfile.from_h(profile_hash) + + # We expect the new profile to be the same + assert_equal profile.address, new_profile.address + assert_equal profile.namespace, new_profile.namespace + assert_equal profile.api_key, new_profile.api_key + assert_equal profile.grpc_meta, new_profile.grpc_meta + + # Test with minimal profile + profile_minimal = Temporalio::EnvConfig::ClientConfigProfile.new + profile_minimal_hash = profile_minimal.to_h + assert_empty profile_minimal_hash + new_profile_minimal = Temporalio::EnvConfig::ClientConfigProfile.from_h(profile_minimal_hash) + assert_nil profile_minimal.address + assert_nil new_profile_minimal.address + end + + def test_client_config_to_from_dict + # Config with multiple profiles + profile1 = Temporalio::EnvConfig::ClientConfigProfile.new( + address: 'some-address', + namespace: 'some-namespace' + ) + profile2 = Temporalio::EnvConfig::ClientConfigProfile.new( + address: 'another-address', + tls: Temporalio::EnvConfig::ClientConfigTLS.new(server_name: 'some-server-name'), + grpc_meta: { 'some-header' => 'some-value' } + ) + config = Temporalio::EnvConfig::ClientConfig.new(profiles: { + 'default' => profile1, + 'custom' => profile2 + }) + + config_hash = config.to_h + + expected_hash = { + 'default' => { + address: 'some-address', + namespace: 'some-namespace' + }, + 'custom' => { + address: 'another-address', + tls: { server_name: 'some-server-name' }, + grpc_meta: { 'some-header' => 'some-value' } + } + } + assert_equal expected_hash, config_hash + + # Convert back to config + new_config = Temporalio::EnvConfig::ClientConfig.from_h(config_hash) + assert_equal config.profiles.keys, new_config.profiles.keys + + # Test empty config + empty_config = Temporalio::EnvConfig::ClientConfig.new(profiles: {}) + empty_config_hash = empty_config.to_h + assert_empty empty_config_hash + new_empty_config = Temporalio::EnvConfig::ClientConfig.from_h(empty_config_hash) + assert_empty new_empty_config.profiles + end + + def test_read_source_from_string_content + # Test that read_source correctly handles string content + profile = Temporalio::EnvConfig::ClientConfigProfile.new( + address: 'localhost:1234', + tls: Temporalio::EnvConfig::ClientConfigTLS.new(client_cert: 'string-as-cert-content') + ) + _, kwargs = profile.to_client_connect_options + tls_config = kwargs[:tls] + assert_instance_of Temporalio::Client::Connection::TLSOptions, tls_config + assert_equal 'string-as-cert-content', tls_config.client_cert + end + + # ============================================================================= + # INTEGRATION/E2E TESTS (2 tests) + # ============================================================================= + + def test_load_client_connect_options_convenience_api + with_temp_config_file(TOML_CONFIG_BASE) do |config_file| + # Test default profile with file + args, = Temporalio::EnvConfig::ClientConfig.load_client_connect_options( + config_source: Pathname.new(config_file) + ) + assert_equal 'default-address', args[0] + assert_equal 'default-namespace', args[1] + + # Test with environment overrides + env = { 'TEMPORAL_NAMESPACE' => 'env-override-namespace' } + args_with_env, = Temporalio::EnvConfig::ClientConfig.load_client_connect_options( + config_source: Pathname.new(config_file), + override_env_vars: env + ) + assert_equal 'default-address', args_with_env[0] + assert_equal 'env-override-namespace', args_with_env[1] + + # Test with specific profile + args_custom, kwargs_custom = Temporalio::EnvConfig::ClientConfig.load_client_connect_options( + profile: 'custom', + config_source: Pathname.new(config_file) + ) + assert_equal 'custom-address', args_custom[0] + assert_equal 'custom-namespace', args_custom[1] + assert_equal 'custom-api-key', kwargs_custom[:api_key] + end + end + + def test_load_client_connect_options_e2e_validation + # Test comprehensive end-to-end configuration loading with all features + toml_content = <<~TOML + [profile.production] + address = "prod.temporal.com:443" + namespace = "production-ns" + api_key = "prod-api-key" + + [profile.production.tls] + server_name = "prod.temporal.com" + server_ca_cert_data = "prod-ca-cert" + + [profile.production.grpc_meta] + authorization = "Bearer prod-token" + "x-custom-header" = "prod-value" + TOML + + env_overrides = { + 'TEMPORAL_GRPC_META_X_ENVIRONMENT' => 'production', + 'TEMPORAL_TLS_SERVER_NAME' => 'override.temporal.com' + } + + args, kwargs = Temporalio::EnvConfig::ClientConfig.load_client_connect_options( + profile: 'production', + config_source: toml_content, + override_env_vars: env_overrides + ) + + # Validate all configuration aspects + assert_equal 'prod.temporal.com:443', args[0] + assert_equal 'production-ns', args[1] + assert_equal 'prod-api-key', kwargs[:api_key] + + # TLS configuration (API key should auto-enable TLS) + refute_nil kwargs[:tls] + tls_config = kwargs[:tls] + assert_equal 'override.temporal.com', tls_config.domain # Env override + assert_equal 'prod-ca-cert', tls_config.server_root_ca_cert + + # gRPC metadata with normalization and env overrides + refute_nil kwargs[:rpc_metadata] + rpc_metadata = kwargs[:rpc_metadata] + assert_equal 'Bearer prod-token', rpc_metadata['authorization'] + assert_equal 'prod-value', rpc_metadata['x-custom-header'] + assert_equal 'production', rpc_metadata['x-environment'] # From env + end + + # ============================================================================= + # END-TO-END CLIENT CONNECTION TESTS (4 tests) + # ============================================================================= + + def test_e2e_basic_development_profile_client_connection + toml_content = <<~TOML + [profile.development] + address = "localhost:7233" + namespace = "dev-namespace" + + [profile.development.grpc_meta] + "x-test-source" = "envconfig-ruby-dev" + TOML + + profile = Temporalio::EnvConfig::ClientConfigProfile.load( + profile: 'development', + config_source: toml_content + ) + + args, kwargs = profile.to_client_connect_options + + # Create actual Temporal client using envconfig + client = Temporalio::Client.connect( + args[0], + args[1], + api_key: kwargs[:api_key], + tls: kwargs[:tls], + rpc_metadata: profile.grpc_meta, + lazy_connect: true + ) + + # Verify client configuration matches envconfig + assert_equal 'localhost:7233', client.connection.target_host + assert_equal 'dev-namespace', client.namespace + assert_equal 'envconfig-ruby-dev', client.connection.options.rpc_metadata['x-test-source'] + end + + def test_e2e_production_tls_api_key_client_connection + toml_content = <<~TOML + [profile.production] + address = "prod.tmprl.cloud:443" + namespace = "production-namespace" + api_key = "prod-api-key-123" + + [profile.production.tls] + server_name = "prod.tmprl.cloud" + + [profile.production.grpc_meta] + authorization = "Bearer prod-token" + "x-environment" = "production" + TOML + + profile = Temporalio::EnvConfig::ClientConfigProfile.load( + profile: 'production', + config_source: toml_content + ) + + args, kwargs = profile.to_client_connect_options + + # Create TLS-enabled client with API key + client = Temporalio::Client.connect( + args[0], + args[1], + api_key: kwargs[:api_key], + tls: kwargs[:tls], + rpc_metadata: profile.grpc_meta, + lazy_connect: true + ) + + # Verify production configuration + assert_equal 'prod.tmprl.cloud:443', client.connection.target_host + assert_equal 'production-namespace', client.namespace + assert_equal 'prod-api-key-123', client.connection.options.api_key + refute_nil client.connection.options.tls # TLS should be enabled with API key + assert_equal 'Bearer prod-token', client.connection.options.rpc_metadata['authorization'] + assert_equal 'production', client.connection.options.rpc_metadata['x-environment'] + end + + def test_e2e_environment_overrides_client_connection + toml_content = <<~TOML + [profile.staging] + address = "staging.temporal.com:443" + namespace = "staging-namespace" + + [profile.staging.grpc_meta] + "x-deployment" = "staging" + authorization = "Bearer staging-token" + TOML + + env_overrides = { + 'TEMPORAL_ADDRESS' => 'override.temporal.com:443', + 'TEMPORAL_NAMESPACE' => 'override-namespace', + 'TEMPORAL_GRPC_META_X_DEPLOYMENT' => 'canary', + 'TEMPORAL_GRPC_META_AUTHORIZATION' => 'Bearer override-token' + } + + profile = Temporalio::EnvConfig::ClientConfigProfile.load( + profile: 'staging', + config_source: toml_content, + override_env_vars: env_overrides + ) + + args, = profile.to_client_connect_options + + # Create client with environment overrides + client = Temporalio::Client.connect( + args[0], + args[1], + rpc_metadata: profile.grpc_meta, + lazy_connect: true + ) + + # Verify environment overrides took effect + assert_equal 'override.temporal.com:443', client.connection.target_host + assert_equal 'override-namespace', client.namespace + assert_equal 'canary', client.connection.options.rpc_metadata['x-deployment'] + assert_equal 'Bearer override-token', client.connection.options.rpc_metadata['authorization'] + end + + def test_e2e_multi_profile_different_client_connections + toml_content = <<~TOML + [profile.development] + address = "localhost:7233" + namespace = "dev" + + [profile.production] + address = "prod.tmprl.cloud:443" + namespace = "prod" + api_key = "prod-key" + + [profile.production.tls] + server_name = "prod.tmprl.cloud" + TOML + + # Load and create development client + dev_profile = Temporalio::EnvConfig::ClientConfigProfile.load( + profile: 'development', + config_source: toml_content + ) + + args_dev, kwargs_dev = dev_profile.to_client_connect_options + dev_client = Temporalio::Client.connect( + args_dev[0], + args_dev[1], + api_key: kwargs_dev[:api_key], + tls: kwargs_dev[:tls], + lazy_connect: true + ) + + # Load and create production client + prod_profile = Temporalio::EnvConfig::ClientConfigProfile.load( + profile: 'production', + config_source: toml_content + ) + + args_prod, kwargs_prod = prod_profile.to_client_connect_options + prod_client = Temporalio::Client.connect( + args_prod[0], + args_prod[1], + api_key: kwargs_prod[:api_key], + tls: kwargs_prod[:tls], + lazy_connect: true + ) + + # Verify different configurations for each client + assert_equal 'localhost:7233', dev_client.connection.target_host + assert_equal 'dev', dev_client.namespace + assert_nil dev_client.connection.options.api_key + assert_nil dev_client.connection.options.tls + + assert_equal 'prod.tmprl.cloud:443', prod_client.connection.target_host + assert_equal 'prod', prod_client.namespace + assert_equal 'prod-key', prod_client.connection.options.api_key + refute_nil prod_client.connection.options.tls # TLS enabled with API key + end + + private + + def with_temp_config_file(content) + Dir.mktmpdir do |tmpdir| # steep:ignore + config_file = File.join(tmpdir, 'config.toml') + File.write(config_file, content) + yield config_file + end + end +end diff --git a/temporalio/test/sig/envconfig_test.rbs b/temporalio/test/sig/envconfig_test.rbs new file mode 100644 index 00000000..4fac79fa --- /dev/null +++ b/temporalio/test/sig/envconfig_test.rbs @@ -0,0 +1,10 @@ +class EnvConfigTest < Test + TOML_CONFIG_BASE: String + TOML_CONFIG_STRICT_FAIL: String + TOML_CONFIG_MALFORMED: String + TOML_CONFIG_TLS_DETAILED: String + + private + + def with_temp_config_file: [T] (String content) { (String config_file) -> T } -> T +end \ No newline at end of file