diff --git a/.gitignore b/.gitignore index 5d34db7b73..e1d0015553 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,7 @@ build/hack/out/ examples/cockroachdb/data playwright-report/ screenshots/ + +# Rust +**/target +/Cargo.lock \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000000..5854bd35b2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +resolver = "2" +members = ["./client/engine"] diff --git a/client/engine/Cargo.lock b/client/engine/Cargo.lock new file mode 100644 index 0000000000..297289e965 --- /dev/null +++ b/client/engine/Cargo.lock @@ -0,0 +1,1340 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" + +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "engine" +version = "0.1.0" +dependencies = [ + "chrono", + "crc32fast", + "futures", + "libc", + "reqwest", + "serde", + "serde_json", + "snafu", + "tokio", + "uuid", +] + +[[package]] +name = "errno" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" + +[[package]] +name = "futures-executor" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" + +[[package]] +name = "futures-macro" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "futures-sink" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" + +[[package]] +name = "futures-task" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" + +[[package]] +name = "futures-util" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" + +[[package]] +name = "h2" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.4.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "itoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "linux-raw-sys" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "openssl" +version = "0.10.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9dfc0783362704e97ef3bd24261995a699468440099ef95d869b4d9732f829a" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f55da20b29f956fb01f0add8683eb26ee13ebe3ebd935e49898717c6b4b2830" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "proc-macro2" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "reqwest" +version = "0.11.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustix" +version = "0.38.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3" +dependencies = [ + "bitflags 2.4.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" + +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "serde_json" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" + +[[package]] +name = "snafu" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" +dependencies = [ + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.5", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "uuid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +dependencies = [ + "getrandom", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.38", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[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.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "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_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[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_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[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_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys", +] diff --git a/client/engine/Cargo.toml b/client/engine/Cargo.toml new file mode 100644 index 0000000000..ee3d360c1d --- /dev/null +++ b/client/engine/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "engine" +version = "0.1.0" +edition = "2021" + +[dependencies] +libc = "0.2.150" +reqwest = { version = "0.11.22", features = ["json", "blocking"] } +tokio = { version = "1.33.0", features = ["full"] } +serde = { version = "1.0.147", features = ["derive"] } +serde_json = { version = "1.0.89", features = ["raw_value"] } +uuid = { version = "1.5.0", features = ["v4"] } +crc32fast = "1.3.2" +snafu = "0.7.5" +chrono = { version = "0.4.31", features = ["serde", "rustc-serialize"] } +futures = "0.3" + +[lib] +name = "engine" +crate-type = ["rlib", "dylib"] diff --git a/client/engine/examples/evaluation.rs b/client/engine/examples/evaluation.rs new file mode 100644 index 0000000000..eff01f8fbb --- /dev/null +++ b/client/engine/examples/evaluation.rs @@ -0,0 +1,24 @@ +// cargo run --example evaluation + +use engine::{self, evaluator}; +use std::collections::HashMap; + +fn main() { + let eng = engine::Engine::new(vec!["default".into()]); + let mut context: HashMap = HashMap::new(); + context.insert("fizz".into(), "buzz".into()); + + let thread = std::thread::spawn(move || loop { + std::thread::sleep(std::time::Duration::from_millis(5000)); + let variant = eng.variant(&evaluator::EvaluationRequest { + namespace_key: "default".into(), + flag_key: "flag1".into(), + entity_id: "entity".into(), + context: context.clone(), + }); + + println!("variant key {}", variant.unwrap().variant_key); + }); + + thread.join().expect("current thread panicked"); +} diff --git a/client/engine/src/common/mod.rs b/client/engine/src/common/mod.rs new file mode 100644 index 0000000000..92036bf80d --- /dev/null +++ b/client/engine/src/common/mod.rs @@ -0,0 +1,75 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +pub enum FlagType { + #[default] + #[serde(rename = "VARIANT_FLAG_TYPE")] + Variant, + #[serde(rename = "BOOLEAN_FLAG_TYPE")] + Boolean, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +pub enum SegmentOperator { + #[default] + #[serde(rename = "OR_SEGMENT_OPERATOR")] + Or, + #[serde(rename = "AND_SEGMENT_OPERATOR")] + And, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +pub enum SegmentMatchType { + #[default] + #[serde(rename = "ANY_SEGMENT_MATCH_TYPE")] + Any, + #[serde(rename = "ALL_SEGMENT_MATCH_TYPE")] + All, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +pub enum ConstraintComparisonType { + #[default] + #[serde(rename = "UNKNOWN_CONSTRAINT_COMPARISON_TYPE")] + Unknown, + #[serde(rename = "STRING_CONSTRAINT_COMPARISON_TYPE")] + String, + #[serde(rename = "NUMBER_CONSTRAINT_COMPARISON_TYPE")] + Number, + #[serde(rename = "BOOLEAN_CONSTRAINT_COMPARISON_TYPE")] + Boolean, + #[serde(rename = "DATETIME_CONSTRAINT_COMPARISON_TYPE")] + DateTime, +} + +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +pub enum RolloutType { + #[default] + #[serde(rename = "UNKNOWN_ROLLOUT_TYPE")] + Unknown, + #[serde(rename = "SEGMENT_ROLLOUT_TYPE")] + Segment, + #[serde(rename = "THRESHOLD_ROLLOUT_TYPE")] + Threshold, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub enum EvaluationReason { + #[default] + #[serde(rename = "UNKNOWN_EVALUATION_REASON")] + Unknown, + #[serde(rename = "FLAG_DISABLED_EVALUATION_REASON")] + FlagDisabled, + #[serde(rename = "MATCH_EVALUATION_REASON")] + Match, + #[serde(rename = "DEFAULT_EVALUATION_REASON")] + Default, +} + +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub enum ErrorEvaluationReason { + #[serde(rename = "UNKNOWN_ERROR_EVALUATION_REASON")] + Unknown, + #[serde(rename = "NOT_FOUND_ERROR_EVALUATION_REASON")] + NotFound, +} diff --git a/client/engine/src/evaluator/mod.rs b/client/engine/src/evaluator/mod.rs new file mode 100644 index 0000000000..1c2251a24b --- /dev/null +++ b/client/engine/src/evaluator/mod.rs @@ -0,0 +1,932 @@ +use chrono::{DateTime, Utc}; +use serde::Serialize; +use snafu::{prelude::*, Whatever}; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; +use std::time::{Duration, SystemTime, SystemTimeError}; + +use crate::common; +use crate::flipt::models; +use crate::store::parsers; +use crate::store::snapshot; +use crate::store::snapshot::Parser; + +const DEFAULT_PERCENT: f32 = 100.0; +const DEFAULT_TOTAL_BUCKET_NUMBER: i32 = 1000; +const DEFAULT_PERCENT_MULTIPIER: f32 = DEFAULT_TOTAL_BUCKET_NUMBER as f32 / DEFAULT_PERCENT; + +pub struct Evaluator +where + T: snapshot::Parser, +{ + flipt_parser: T, + snapshot: snapshot::Snapshot, + mtx: Arc>, +} + +#[repr(C)] +pub struct EvaluationRequest { + pub namespace_key: String, + pub flag_key: String, + pub entity_id: String, + pub context: HashMap, +} + +#[derive(Serialize)] +pub struct VariantEvaluationResponse { + pub r#match: bool, + pub segment_keys: Vec, + pub reason: common::EvaluationReason, + pub flag_key: String, + pub variant_key: String, + pub variant_attachment: String, + pub request_duration_millis: f64, + pub timestamp: DateTime, +} + +pub struct BooleanEvaluationResponse { + pub enabled: bool, + pub flag_key: String, + pub reason: common::EvaluationReason, + pub request_duration_millis: f64, + pub timestamp: DateTime, +} + +pub struct ErrorEvaluationResponse { + pub flag_key: String, + pub reason: common::ErrorEvaluationReason, +} + +pub trait EvaluationResponse {} + +impl EvaluationResponse for VariantEvaluationResponse {} +impl EvaluationResponse for BooleanEvaluationResponse {} +impl EvaluationResponse for ErrorEvaluationResponse {} + +impl Default for VariantEvaluationResponse { + fn default() -> Self { + Self { + r#match: false, + segment_keys: Vec::new(), + reason: common::EvaluationReason::Unknown, + flag_key: String::from(""), + variant_key: String::from(""), + variant_attachment: String::from(""), + request_duration_millis: 0.0, + timestamp: chrono::offset::Utc::now(), + } + } +} + +impl Default for BooleanEvaluationResponse { + fn default() -> Self { + Self { + enabled: false, + flag_key: String::from(""), + reason: common::EvaluationReason::Unknown, + request_duration_millis: 0.0, + timestamp: chrono::offset::Utc::now(), + } + } +} + +impl Default for ErrorEvaluationResponse { + fn default() -> Self { + Self { + flag_key: String::from(""), + reason: common::ErrorEvaluationReason::Unknown, + } + } +} + +type VariantEvaluationResult = std::result::Result; + +type BooleanEvaluationResult = std::result::Result; + +impl Evaluator { + pub fn new(namespaces: Vec) -> Result { + let flipt_parser = parsers::FliptParser::new(namespaces.clone()); + let snap = snapshot::Snapshot::build(&flipt_parser)?; + + Ok(Self { + flipt_parser, + snapshot: snap, + mtx: Arc::new(RwLock::new(0)), + }) + } + + pub fn replace_snapshot(&mut self) { + let _w_lock = self.mtx.write().unwrap(); + let snap = snapshot::Snapshot::build(&self.flipt_parser); + self.snapshot = snap.unwrap(); + } + + pub fn variant( + &self, + evaluation_request: &EvaluationRequest, + ) -> VariantEvaluationResult { + let _r_lock = self.mtx.read().unwrap(); + let flag = match self.snapshot.get_flag( + &evaluation_request.namespace_key, + &evaluation_request.flag_key, + ) { + Some(f) => { + if f.r#type != common::FlagType::Variant { + whatever!("{} is not a variant flag", &evaluation_request.flag_key); + } + f + } + None => whatever!( + "failed to get flag information {}/{}", + &evaluation_request.namespace_key, + &evaluation_request.flag_key + ), + }; + + self.variant_evaluation(&flag, evaluation_request) + } + + pub fn boolean( + &self, + evaluation_request: &EvaluationRequest, + ) -> BooleanEvaluationResult { + let _r_lock = self.mtx.read().unwrap(); + let flag = match self.snapshot.get_flag( + &evaluation_request.namespace_key, + &evaluation_request.flag_key, + ) { + Some(f) => { + if f.r#type != common::FlagType::Boolean { + whatever!("{} is not a boolean flag", &evaluation_request.flag_key); + } + f + } + None => whatever!( + "failed to get flag information {}/{}", + &evaluation_request.namespace_key, + &evaluation_request.flag_key + ), + }; + + self.boolean_evaluation(&flag, evaluation_request) + } + + pub fn batch( + &self, + requests: Vec, + ) -> Result>, Whatever> { + let mut evaluation_responses: Vec> = Vec::new(); + + for request in requests { + let flag = match self + .snapshot + .get_flag(&request.namespace_key, &request.flag_key) + { + Some(f) => f, + None => { + evaluation_responses.push(Box::new(ErrorEvaluationResponse { + flag_key: request.flag_key.clone(), + reason: common::ErrorEvaluationReason::NotFound, + })); + continue; + } + }; + + match flag.r#type { + common::FlagType::Boolean => { + match self.boolean_evaluation(&flag, &request) { + Ok(b) => { + evaluation_responses.push(Box::new(b)); + } + Err(e) => { + return Err(e); + } + }; + } + common::FlagType::Variant => { + match self.variant_evaluation(&flag, &request) { + Ok(v) => { + evaluation_responses.push(Box::new(v)); + } + Err(e) => { + return Err(e); + } + }; + } + } + } + + Ok(evaluation_responses) + } + + fn variant_evaluation( + &self, + flag: &models::Flag, + evaluation_request: &EvaluationRequest, + ) -> VariantEvaluationResult { + let now = SystemTime::now(); + let mut last_rank = 0; + + let mut variant_evaluation_response = VariantEvaluationResponse { + flag_key: flag.key.clone(), + ..Default::default() + }; + + if !flag.enabled { + variant_evaluation_response.reason = common::EvaluationReason::FlagDisabled; + variant_evaluation_response.request_duration_millis = get_duration(now.elapsed())?; + return Ok(variant_evaluation_response); + } + + let evaluation_rules = match self.snapshot.get_evaluation_rules( + &evaluation_request.namespace_key, + &evaluation_request.flag_key, + ) { + Some(evaluation_rules) => evaluation_rules, + None => whatever!( + "error getting evaluation rules for namespace {} and flag {}", + evaluation_request.namespace_key.clone(), + evaluation_request.flag_key.clone() + ), + }; + + for rule in evaluation_rules { + if rule.rank < last_rank { + whatever!("rule rank: {} detected out of order", rule.rank); + } + + last_rank = rule.rank; + + let mut segment_keys: Vec = Vec::new(); + let mut segment_matches = 0; + + for kv in &rule.segments { + let matched = match self.matches_constraints( + &evaluation_request.context, + &kv.1.constraints, + &kv.1.match_type, + ) { + Ok(b) => b, + Err(err) => return Err(err), + }; + + if matched { + segment_keys.push(kv.0.into()); + segment_matches += 1; + } + } + + if rule.segment_operator == common::SegmentOperator::Or { + if segment_matches < 1 { + continue; + } + } else if rule.segment_operator == common::SegmentOperator::And + && rule.segments.len() != segment_matches + { + continue; + } + + variant_evaluation_response.segment_keys = segment_keys; + + let distributions = match self + .snapshot + .get_evaluation_distributions(&evaluation_request.namespace_key, &rule.id) + { + Some(evaluation_distributions) => evaluation_distributions, + None => whatever!( + "error getting evaluation distributions for namespace {} and rule {}", + evaluation_request.namespace_key.clone(), + rule.id.clone() + ), + }; + + let mut valid_distributions: Vec = Vec::new(); + let mut buckets: Vec = Vec::new(); + + for distribution in distributions { + if distribution.rollout > 0.0 { + valid_distributions.push(distribution.clone()); + } + + if buckets.is_empty() { + let bucket = (distribution.rollout * DEFAULT_PERCENT_MULTIPIER) as i32; + buckets.push(bucket); + } else { + let bucket = buckets[buckets.len() - 1] + + (distribution.rollout * DEFAULT_PERCENT_MULTIPIER) as i32; + buckets.push(bucket); + } + } + + // no distributions for the rule + if valid_distributions.is_empty() { + variant_evaluation_response.r#match = true; + variant_evaluation_response.reason = common::EvaluationReason::Match; + variant_evaluation_response.request_duration_millis = get_duration(now.elapsed())?; + return Ok(variant_evaluation_response); + } + + let bucket = crc32fast::hash( + format!( + "{}{}", + evaluation_request.entity_id, evaluation_request.flag_key + ) + .as_bytes(), + ) as i32 + % DEFAULT_TOTAL_BUCKET_NUMBER; + + buckets.sort(); + + let index = match buckets.binary_search(&bucket) { + Ok(idx) => idx, + Err(idx) => idx, + }; + + if index == valid_distributions.len() { + variant_evaluation_response.r#match = false; + variant_evaluation_response.request_duration_millis = get_duration(now.elapsed())?; + return Ok(variant_evaluation_response); + } + + let d = &valid_distributions[index]; + + variant_evaluation_response.r#match = true; + variant_evaluation_response.variant_key = d.variant_key.clone(); + variant_evaluation_response.variant_attachment = d.variant_attachment.clone(); + variant_evaluation_response.reason = common::EvaluationReason::Match; + variant_evaluation_response.request_duration_millis = get_duration(now.elapsed())?; + return Ok(variant_evaluation_response); + } + + Ok(variant_evaluation_response) + } + + fn boolean_evaluation( + &self, + flag: &models::Flag, + evaluation_request: &EvaluationRequest, + ) -> BooleanEvaluationResult { + let now = SystemTime::now(); + let mut last_rank = 0; + + let evaluation_rollouts = match self.snapshot.get_evaluation_rollouts( + &evaluation_request.namespace_key, + &evaluation_request.flag_key, + ) { + Some(rollouts) => rollouts, + None => whatever!( + "error getting evaluation rollouts for namespace {} and flag {}", + evaluation_request.namespace_key.clone(), + evaluation_request.flag_key.clone() + ), + }; + + for rollout in evaluation_rollouts { + if rollout.rank < last_rank { + whatever!("rollout rank: {} detected out of order", rollout.rank); + } + + last_rank = rollout.rank; + + if rollout.threshold.is_some() { + let threshold = rollout.threshold.unwrap(); + + let normalized_value = (crc32fast::hash( + format!( + "{}{}", + evaluation_request.entity_id, evaluation_request.flag_key + ) + .as_bytes(), + ) as i32 + % 100) as f32; + + if normalized_value < threshold.percentage { + return Ok(BooleanEvaluationResponse { + enabled: threshold.value, + flag_key: flag.key.clone(), + reason: common::EvaluationReason::Match, + request_duration_millis: get_duration(now.elapsed())?, + timestamp: chrono::offset::Utc::now(), + }); + } + } else if rollout.segment.is_some() { + let segment = rollout.segment.unwrap(); + let mut segment_matches = 0; + + for s in &segment.segments { + let matched = match self.matches_constraints( + &evaluation_request.context, + &s.1.constraints, + &s.1.match_type, + ) { + Ok(v) => v, + Err(err) => return Err(err), + }; + + if matched { + segment_matches += 1; + } + } + + if segment.segment_operator == common::SegmentOperator::Or { + if segment_matches < 1 { + continue; + } + } else if segment.segment_operator == common::SegmentOperator::And + && segment.segments.len() != segment_matches + { + continue; + } + + return Ok(BooleanEvaluationResponse { + enabled: segment.value, + flag_key: flag.key.clone(), + reason: common::EvaluationReason::Match, + request_duration_millis: get_duration(now.elapsed())?, + timestamp: chrono::offset::Utc::now(), + }); + } + } + + Ok(BooleanEvaluationResponse { + enabled: flag.enabled, + flag_key: flag.key.clone(), + reason: common::EvaluationReason::Default, + request_duration_millis: get_duration(now.elapsed())?, + timestamp: chrono::offset::Utc::now(), + }) + } + + fn matches_constraints( + &self, + eval_context: &HashMap, + constraints: &Vec, + segment_match_type: &common::SegmentMatchType, + ) -> Result { + let mut constraint_matches: usize = 0; + + for constraint in constraints { + let value = match eval_context.get(&constraint.property) { + Some(v) => v, + None => continue, + }; + + let matched = match constraint.r#type { + common::ConstraintComparisonType::String => matches_string(constraint, value), + common::ConstraintComparisonType::Number => matches_number(constraint, value)?, + common::ConstraintComparisonType::Boolean => matches_boolean(constraint, value)?, + common::ConstraintComparisonType::DateTime => matches_datetime(constraint, value)?, + _ => { + return Ok(false); + } + }; + + if matched { + constraint_matches += 1; + + if segment_match_type == &common::SegmentMatchType::Any { + break; + } else { + continue; + } + } else if segment_match_type == &common::SegmentMatchType::All { + break; + } else { + continue; + } + } + + let is_match = match segment_match_type { + common::SegmentMatchType::All => constraints.len() == constraint_matches, + common::SegmentMatchType::Any => constraints.is_empty() || constraint_matches != 0, + }; + + Ok(is_match) + } +} + +fn matches_string(evaluation_constraint: &models::EvaluationConstraint, v: &str) -> bool { + let operator = evaluation_constraint.operator.as_str(); + + match operator { + "empty" => { + return v.is_empty(); + } + "notempty" => { + return !v.is_empty(); + } + _ => {} + } + + if v.is_empty() { + return false; + } + + let value = evaluation_constraint.value.as_str(); + match operator { + "eq" => v == value, + "neq" => v != value, + "prefix" => v.starts_with(value), + "suffix" => v.ends_with(value), + _ => false, + } +} + +fn matches_number( + evaluation_constraint: &models::EvaluationConstraint, + v: &str, +) -> Result { + let operator = evaluation_constraint.operator.as_str(); + + match operator { + "notpresent" => { + return Ok(v.is_empty()); + } + "present" => { + return Ok(!v.is_empty()); + } + _ => {} + } + + if v.is_empty() { + return Ok(false); + } + + let v_number = match v.parse::() { + Ok(v) => v, + Err(err) => whatever!("error parsing number {}, err: {}", v, err), + }; + + let value_number = match evaluation_constraint.value.parse::() { + Ok(v) => v, + Err(err) => whatever!( + "error parsing number {}, err: {}", + evaluation_constraint.value, + err + ), + }; + + match operator { + "eq" => Ok(v_number == value_number), + "neq" => Ok(v_number != value_number), + "lt" => Ok(v_number < value_number), + "lte" => Ok(v_number <= value_number), + "gt" => Ok(v_number > value_number), + "gte" => Ok(v_number >= value_number), + _ => Ok(false), + } +} + +fn matches_boolean( + evaluation_constraint: &models::EvaluationConstraint, + v: &str, +) -> Result { + let operator = evaluation_constraint.operator.as_str(); + + match operator { + "notpresent" => { + return Ok(v.is_empty()); + } + "present" => { + return Ok(!v.is_empty()); + } + _ => {} + } + + if v.is_empty() { + return Ok(false); + } + + let v_bool = match v.parse::() { + Ok(v) => v, + Err(err) => whatever!("error parsing boolean {}: err {}", v, err), + }; + + match operator { + "true" => Ok(v_bool), + "false" => Ok(!v_bool), + _ => Ok(false), + } +} + +fn matches_datetime( + evaluation_constraint: &models::EvaluationConstraint, + v: &str, +) -> Result { + let operator = evaluation_constraint.operator.as_str(); + + match operator { + "notpresent" => { + return Ok(v.is_empty()); + } + "present" => { + return Ok(!v.is_empty()); + } + _ => {} + } + + if v.is_empty() { + return Ok(false); + } + + let d = match DateTime::parse_from_rfc3339(v) { + Ok(t) => t.timestamp(), + Err(e) => whatever!("error parsing time {}, err: {}", v, e), + }; + + let value = match DateTime::parse_from_rfc3339(&evaluation_constraint.value) { + Ok(t) => t.timestamp(), + Err(e) => whatever!( + "error parsing time {}, err: {}", + &evaluation_constraint.value, + e + ), + }; + + match operator { + "eq" => Ok(d == value), + "neq" => Ok(d != value), + "lt" => Ok(d < value), + "lte" => Ok(d <= value), + "gt" => Ok(d > value), + "gte" => Ok(d >= value), + _ => Ok(false), + } +} + +fn get_duration(elapsed: Result) -> Result { + match elapsed { + Ok(elapsed) => Ok(elapsed.as_secs_f64()), + Err(e) => { + whatever!("error getting duration {}", e) + } + } +} + +#[cfg(test)] +mod tests { + use super::{matches_boolean, matches_datetime, matches_number, matches_string}; + use crate::common; + use crate::flipt::models as flipt_models; + + macro_rules! matches_string_tests { + ($($name:ident: $value:expr,)*) => { + $( + #[test] + fn $name() { + let (first, second, expected) = $value; + assert_eq!(expected, matches_string(first, second)); + } + )* + } + } + + macro_rules! matches_datetime_tests { + ($($name:ident: $value:expr,)*) => { + $( + #[test] + fn $name() { + let (first, second, expected) = $value; + assert_eq!(expected, matches_datetime(first, second).unwrap()); + } + )* + } + } + + macro_rules! matches_number_tests { + ($($name:ident: $value:expr,)*) => { + $( + #[test] + fn $name() { + let (first, second, expected) = $value; + assert_eq!(expected, matches_number(first, second).unwrap()); + } + )* + } + } + + matches_string_tests! { + string_eq: (&flipt_models::EvaluationConstraint{ + r#type: common::ConstraintComparisonType::String, + property: String::from("number"), + operator: String::from("eq"), + value: String::from("number"), + }, "number", true), + string_neq: (&flipt_models::EvaluationConstraint{ + r#type: common::ConstraintComparisonType::String, + property: String::from("number"), + operator: String::from("neq"), + value: String::from("number"), + }, "num", true), + string_prefix: (&flipt_models::EvaluationConstraint{ + r#type: common::ConstraintComparisonType::String, + property: String::from("number"), + operator: String::from("prefix"), + value: String::from("num"), + }, "number", true), + string_suffix: (&flipt_models::EvaluationConstraint{ + r#type: common::ConstraintComparisonType::String, + property: String::from("number"), + operator: String::from("suffix"), + value: String::from("ber"), + }, "number", true), + } + + matches_datetime_tests! { + datetime_eq: (&flipt_models::EvaluationConstraint{ + r#type: common::ConstraintComparisonType::DateTime, + property: String::from("date"), + operator: String::from("eq"), + value: String::from("2006-01-02T15:04:05Z"), + }, "2006-01-02T15:04:05Z", true), + datetime_neq: (&flipt_models::EvaluationConstraint{ + r#type: common::ConstraintComparisonType::DateTime, + property: String::from("date"), + operator: String::from("neq"), + value: String::from("2006-01-02T15:04:05Z"), + }, "2006-01-02T15:03:05Z", true), + datetime_lt: (&flipt_models::EvaluationConstraint{ + r#type: common::ConstraintComparisonType::DateTime, + property: String::from("date"), + operator: String::from("lt"), + value: String::from("2006-01-02T15:04:05Z"), + }, "2006-01-02T14:03:05Z", true), + datetime_gt: (&flipt_models::EvaluationConstraint{ + r#type: common::ConstraintComparisonType::DateTime, + property: String::from("date"), + operator: String::from("gt"), + value: String::from("2006-01-02T15:04:05Z"), + }, "2006-01-02T16:03:05Z", true), + datetime_lte: (&flipt_models::EvaluationConstraint{ + r#type: common::ConstraintComparisonType::DateTime, + property: String::from("date"), + operator: String::from("lte"), + value: String::from("2006-01-02T15:04:05Z"), + }, "2006-01-02T15:04:05Z", true), + datetime_gte: (&flipt_models::EvaluationConstraint{ + r#type: common::ConstraintComparisonType::DateTime, + property: String::from("date"), + operator: String::from("gte"), + value: String::from("2006-01-02T15:04:05Z"), + }, "2006-01-02T16:03:05Z", true), + + } + + matches_number_tests! { + number_eq: (&flipt_models::EvaluationConstraint{ + r#type: common::ConstraintComparisonType::Number, + property: String::from("number"), + operator: String::from("eq"), + value: String::from("1"), + }, "1", true), + number_neq: (&flipt_models::EvaluationConstraint{ + r#type: common::ConstraintComparisonType::Number, + property: String::from("number"), + operator: String::from("neq"), + value: String::from("1"), + }, "0", true), + number_lt: (&flipt_models::EvaluationConstraint{ + r#type: common::ConstraintComparisonType::Number, + property: String::from("number"), + operator: String::from("lt"), + value: String::from("4"), + }, "3", true), + number_gt: (&flipt_models::EvaluationConstraint{ + r#type: common::ConstraintComparisonType::Number, + property: String::from("number"), + operator: String::from("gt"), + value: String::from("3"), + }, "4", true), + number_lte: (&flipt_models::EvaluationConstraint{ + r#type: common::ConstraintComparisonType::Number, + property: String::from("date"), + operator: String::from("lte"), + value: String::from("3"), + }, "3", true), + number_gte: (&flipt_models::EvaluationConstraint{ + r#type: common::ConstraintComparisonType::Number, + property: String::from("date"), + operator: String::from("gte"), + value: String::from("3"), + }, "4", true), + + } + + #[test] + fn test_matches_boolean_success() { + let value_one = matches_boolean( + &flipt_models::EvaluationConstraint { + r#type: common::ConstraintComparisonType::Boolean, + property: String::from("fizz"), + operator: String::from("true"), + value: "".into(), + }, + "true", + ) + .expect("boolean should be parsed correctly"); + + assert!(value_one); + + let value_two = matches_boolean( + &flipt_models::EvaluationConstraint { + r#type: common::ConstraintComparisonType::Boolean, + property: String::from("fizz"), + operator: String::from("false"), + value: "".into(), + }, + "false", + ) + .expect("boolean should be parsed correctly"); + + assert!(value_two); + } + + #[test] + fn test_matches_boolean_failure() { + let result = matches_boolean( + &flipt_models::EvaluationConstraint { + r#type: common::ConstraintComparisonType::Boolean, + property: String::from("fizz"), + operator: String::from("true"), + value: "".into(), + }, + "blah", + ); + + assert!(!result.is_ok()); + assert_eq!( + result.err().unwrap().to_string(), + "error parsing boolean blah: err provided string was not `true` or `false`" + ); + } + + #[test] + fn test_matches_number_failure() { + let result_one = matches_number( + &flipt_models::EvaluationConstraint { + r#type: common::ConstraintComparisonType::Number, + property: String::from("number"), + operator: String::from("eq"), + value: String::from("9"), + }, + "notanumber", + ); + + assert!(!result_one.is_ok()); + assert_eq!( + result_one.err().unwrap().to_string(), + "error parsing number notanumber, err: invalid digit found in string" + ); + + let result_two = matches_number( + &flipt_models::EvaluationConstraint { + r#type: common::ConstraintComparisonType::Number, + property: String::from("number"), + operator: String::from("eq"), + value: String::from("notanumber"), + }, + "9", + ); + + assert!(!result_two.is_ok()); + assert_eq!( + result_two.err().unwrap().to_string(), + "error parsing number notanumber, err: invalid digit found in string" + ); + } + + #[test] + fn test_matches_datetime_failure() { + let result_one = matches_datetime( + &flipt_models::EvaluationConstraint { + r#type: common::ConstraintComparisonType::String, + property: String::from("date"), + operator: String::from("eq"), + value: String::from("blah"), + }, + "2006-01-02T15:04:05Z", + ); + + assert!(!result_one.is_ok()); + assert_eq!( + result_one.err().unwrap().to_string(), + "error parsing time blah, err: input contains invalid characters" + ); + + let result_two = matches_datetime( + &flipt_models::EvaluationConstraint { + r#type: common::ConstraintComparisonType::String, + property: String::from("date"), + operator: String::from("eq"), + value: String::from("2006-01-02T15:04:05Z"), + }, + "blah", + ); + + assert!(!result_two.is_ok()); + assert_eq!( + result_two.err().unwrap().to_string(), + "error parsing time blah, err: input contains invalid characters" + ); + } +} diff --git a/client/engine/src/flipt/mod.rs b/client/engine/src/flipt/mod.rs new file mode 100644 index 0000000000..c446ac8833 --- /dev/null +++ b/client/engine/src/flipt/mod.rs @@ -0,0 +1 @@ +pub mod models; diff --git a/client/engine/src/flipt/models.rs b/client/engine/src/flipt/models.rs new file mode 100644 index 0000000000..ce55936750 --- /dev/null +++ b/client/engine/src/flipt/models.rs @@ -0,0 +1,78 @@ +use crate::common; +use serde::Deserialize; +use std::collections::HashMap; + +#[derive(Clone)] +pub struct Flag { + pub key: String, + pub enabled: bool, + pub r#type: common::FlagType, +} + +#[derive(Clone, Deserialize)] +pub struct Variant { + pub key: String, + pub attachment: String, +} + +#[derive(Clone)] +pub struct Constraint { + pub segment_key: String, + pub r#type: common::ConstraintComparisonType, + pub property: String, + pub operator: String, + pub value: String, +} + +#[derive(Clone, Debug)] +pub struct EvaluationRule { + pub id: String, + pub flag_key: String, + pub segments: HashMap, + pub rank: usize, + pub segment_operator: common::SegmentOperator, +} + +#[derive(Clone, Debug)] +pub struct EvaluationDistribution { + pub rule_id: String, + pub rollout: f32, + pub variant_key: String, + pub variant_attachment: String, +} + +#[derive(Clone, Debug)] +pub struct EvaluationRollout { + pub rollout_type: common::RolloutType, + pub rank: usize, + pub segment: Option, + pub threshold: Option, +} + +#[derive(Clone, Debug)] +pub struct RolloutThreshold { + pub percentage: f32, + pub value: bool, +} + +#[derive(Clone, Debug)] +pub struct RolloutSegment { + pub value: bool, + pub segment_operator: common::SegmentOperator, + pub segments: HashMap, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct EvaluationSegment { + pub segment_key: String, + pub match_type: common::SegmentMatchType, + pub constraints: Vec, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct EvaluationConstraint { + pub r#type: common::ConstraintComparisonType, + pub property: String, + pub operator: String, + pub value: String, +} diff --git a/client/engine/src/lib.rs b/client/engine/src/lib.rs new file mode 100644 index 0000000000..c6e0e0f95b --- /dev/null +++ b/client/engine/src/lib.rs @@ -0,0 +1,163 @@ +use libc::c_void; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use snafu::{prelude::*, Whatever}; +use std::collections::HashMap; +use std::ffi::{CStr, CString}; +use std::os::raw::c_char; +use std::sync::{Arc, Mutex}; + +mod common; +pub mod evaluator; +mod flipt; +mod store; + +#[derive(Deserialize)] +struct EvaluationReq { + namespace_key: String, + flag_key: String, + entity_id: String, + context: String, +} + +pub struct Engine { + pub evaluator: Arc>>, +} + +impl Engine { + pub fn new(namespaces: Vec) -> Self { + let evaluation_engine = evaluator::Evaluator::new(namespaces); + + let mut evaluator = Self { + evaluator: Arc::new(Mutex::new(evaluation_engine.unwrap())), + }; + + evaluator.update(); + + evaluator + } + + fn update(&mut self) { + let evaluator = self.evaluator.clone(); + let update_interval = std::env::var("FLIPT_UPDATE_INTERVAL") + .unwrap_or("120".into()) + .parse::() + .unwrap_or(120); + + std::thread::spawn(move || loop { + std::thread::sleep(std::time::Duration::from_secs(update_interval)); + let mut lock = evaluator.lock().unwrap(); + lock.replace_snapshot(); + }); + } + + pub fn variant( + &self, + evaluation_request: &evaluator::EvaluationRequest, + ) -> Result { + let binding = self.evaluator.clone(); + let lock = binding.lock().unwrap(); + + lock.variant(evaluation_request) + } + + pub fn boolean( + &self, + evaluation_request: &evaluator::EvaluationRequest, + ) -> Result { + let binding = self.evaluator.clone(); + let lock = binding.lock().unwrap(); + + lock.boolean(evaluation_request) + } +} + +fn result_to_json_ptr(result: T) -> *mut c_char { + let json_string = serde_json::to_string(&result).unwrap(); + CString::new(json_string).unwrap().into_raw() +} + +/// # Safety +/// +/// This function should not be called unless an Engine is initiated. It provides a helper +/// utility to retrieve an Engine instance for evaluation use. +unsafe fn get_engine<'a>(engine_ptr: *mut c_void) -> Result<&'a mut Engine, Whatever> { + if engine_ptr.is_null() { + whatever!("null pointer engine error"); + } else { + Ok(unsafe { &mut *(engine_ptr as *mut Engine) }) + } +} + +/// # Safety +/// +/// This function will initialize an Engine and return a pointer back to the caller. +#[no_mangle] +pub unsafe extern "C" fn initialize_engine(namespaces: *const *const c_char) -> *mut c_void { + let mut index = 0; + let mut namespaces_vec = Vec::new(); + + while !(*namespaces.offset(index)).is_null() { + let c_str = CStr::from_ptr(*namespaces.offset(index)); + if let Ok(rust_str) = c_str.to_str() { + namespaces_vec.push(rust_str.to_string()); + } + + index += 1; + } + + Box::into_raw(Box::new(Engine::new(namespaces_vec))) as *mut c_void +} + +/// # Safety +/// +/// This function will take in an engine pointer and return an evalution response for boolean or variant. +#[no_mangle] +pub unsafe extern "C" fn variant( + engine_ptr: *mut c_void, + evaluation_request: *const c_char, +) -> *const c_char { + let bytes = unsafe { CStr::from_ptr(evaluation_request).to_bytes() }; + let e_req = std::str::from_utf8(bytes).unwrap(); + + let client_eval_request: EvaluationReq = serde_json::from_str(e_req).unwrap(); + + let parsed_context: serde_json::Value = + serde_json::from_str(&client_eval_request.context).unwrap(); + + let mut context_map: HashMap = HashMap::new(); + if let serde_json::Value::Object(map) = parsed_context { + for (key, value) in map { + if let Value::String(val) = value { + context_map.insert(key, val); + } + } + } + + let e = get_engine(engine_ptr).unwrap(); + + let variant_response = e + .variant(&evaluator::EvaluationRequest { + namespace_key: client_eval_request.namespace_key, + flag_key: client_eval_request.flag_key, + entity_id: client_eval_request.entity_id, + context: HashMap::new(), + }) + .unwrap(); + + println!("{}", variant_response.variant_key); + + result_to_json_ptr(variant_response) +} + +/// # Safety +/// +/// This function will free the memory occupied by the engine. +#[no_mangle] +pub unsafe extern "C" fn destroy_engine(engine_ptr: *mut c_void) { + if engine_ptr.is_null() { + return; + } + + drop(Box::from_raw(engine_ptr as *mut Engine)); +} diff --git a/client/engine/src/store/mod.rs b/client/engine/src/store/mod.rs new file mode 100644 index 0000000000..334fb0630f --- /dev/null +++ b/client/engine/src/store/mod.rs @@ -0,0 +1,3 @@ +pub mod models; +pub mod parsers; +pub mod snapshot; diff --git a/client/engine/src/store/models.rs b/client/engine/src/store/models.rs new file mode 100644 index 0000000000..edc776794f --- /dev/null +++ b/client/engine/src/store/models.rs @@ -0,0 +1,78 @@ +use crate::common; +use serde::Deserialize; + +#[derive(Deserialize)] +pub struct Document { + pub namespace: Namespace, + pub flags: Vec, +} + +#[derive(Deserialize)] +pub struct Namespace { + pub key: String, + pub name: String, +} + +#[derive(Deserialize)] +pub struct Flag { + pub key: String, + pub name: String, + pub r#type: Option, + pub description: Option, + pub enabled: bool, + pub rules: Option>, + pub rollouts: Option>, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Rule { + pub distributions: Vec, + pub segments: Option>, + pub segment_operator: common::SegmentOperator, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Distribution { + pub variant_key: String, + pub rollout: f32, + pub variant_attachment: String, +} + +#[derive(Deserialize)] +pub struct Rollout { + pub description: Option, + pub segment: Option, + pub threshold: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SegmentRule { + pub segment_operator: Option, + pub value: bool, + pub segments: Vec, +} + +#[derive(Deserialize)] +pub struct Threshold { + pub percentage: f32, + pub value: bool, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Segment { + pub key: String, + pub match_type: common::SegmentMatchType, + pub constraints: Vec, +} + +#[derive(Deserialize)] +pub struct SegmentConstraint { + pub r#type: common::ConstraintComparisonType, + pub property: String, + pub operator: String, + pub value: String, +} diff --git a/client/engine/src/store/parsers.rs b/client/engine/src/store/parsers.rs new file mode 100644 index 0000000000..97fb36d62d --- /dev/null +++ b/client/engine/src/store/parsers.rs @@ -0,0 +1,91 @@ +use snafu::{prelude::*, Whatever}; +use std::env; +use std::fs; +use std::path::PathBuf; + +use super::models; +use super::snapshot::Parser; + +pub struct FliptParser { + namespaces: Vec, + http_client: reqwest::blocking::Client, + http_url: String, +} + +impl Parser for FliptParser { + fn new(namespaces: Vec) -> Self { + // We will allow the following line to panic when an error is encountered. + let http_client = reqwest::blocking::Client::builder() + .timeout(std::time::Duration::from_secs(10)) + .build() + .unwrap(); + + let http_url = + env::var("FLIPT_REMOTE_URL").unwrap_or(String::from("http://localhost:8080")); + + Self { + namespaces, + http_client, + http_url, + } + } + + fn parse(&self, namespace: String) -> Result { + let response = match self + .http_client + .get(format!( + "{}/internal/v1/evaluation/snapshot/namespace/{}", + self.http_url, namespace + )) + .send() + { + Ok(resp) => resp, + Err(e) => whatever!("failed to make request: err {}", e), + }; + + let response_text = match response.text() { + Ok(t) => t, + Err(e) => whatever!("failed to get response body: err {}", e), + }; + + let document: models::Document = match serde_json::from_str(&response_text) { + Ok(doc) => doc, + Err(e) => whatever!("failed to deserialize text into document: err {}", e), + }; + + Ok(document) + } + + fn get_namespaces(&self) -> Vec { + self.namespaces.clone() + } +} + +pub struct TestParser { + namespaces: Vec, +} + +impl Parser for TestParser { + fn new(namespaces: Vec) -> Self { + Self { namespaces } + } + + fn parse(&self, _: String) -> Result { + let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + d.push("src/testdata/state.json"); + + let state = + fs::read_to_string(d.display().to_string()).expect("file should have read correctly"); + + let document: models::Document = match serde_json::from_str(&state) { + Ok(document) => document, + Err(e) => whatever!("failed to deserialize text into document: err {}", e), + }; + + Ok(document) + } + + fn get_namespaces(&self) -> Vec { + self.namespaces.clone() + } +} diff --git a/client/engine/src/store/snapshot.rs b/client/engine/src/store/snapshot.rs new file mode 100644 index 0000000000..604cf78f96 --- /dev/null +++ b/client/engine/src/store/snapshot.rs @@ -0,0 +1,353 @@ +use super::models; +use crate::common; +use crate::flipt::models as flipt_models; +use snafu::Whatever; +use std::collections::HashMap; + +pub trait Parser { + fn new(namespaces: Vec) -> Self; + fn parse(&self, namespace: String) -> Result; + fn get_namespaces(&self) -> Vec; +} + +pub struct Snapshot { + namespace: HashMap, +} + +struct Namespace { + _key: String, + flags: HashMap, + eval_rules: HashMap>, + eval_rollouts: HashMap>, + eval_distributions: HashMap>, +} + +impl Snapshot { + pub fn build(parser: &T) -> Result + where + T: Parser, + { + let mut ns: HashMap = HashMap::new(); + + for n in parser.get_namespaces() { + let doc = parser.parse(n.clone())?; + + let mut flags: HashMap = HashMap::new(); + let mut eval_rules: HashMap> = HashMap::new(); + let mut eval_rollouts: HashMap> = + HashMap::new(); + let mut eval_dists: HashMap> = + HashMap::new(); + + for flag in doc.flags { + let f = flipt_models::Flag { + key: flag.key.clone(), + enabled: flag.enabled, + r#type: flag.r#type.unwrap_or(common::FlagType::Variant), + }; + + flags.insert(f.key.clone(), f); + + // Flag Rules + let mut eval_rules_collection: Vec = Vec::new(); + + let flag_rules = flag.rules.unwrap_or(vec![]); + + for (idx, rule) in flag_rules.into_iter().enumerate() { + let rule_id = uuid::Uuid::new_v4().to_string(); + let mut eval_rule = flipt_models::EvaluationRule { + id: rule_id.clone(), + rank: idx + 1, + flag_key: flag.key.clone(), + segments: HashMap::new(), + segment_operator: rule.segment_operator, + }; + + if rule.segments.is_some() { + let rule_segments = rule.segments.unwrap(); + + for rule_segment in rule_segments { + let mut eval_constraints: Vec = + Vec::new(); + for constraint in rule_segment.constraints { + eval_constraints.push(flipt_models::EvaluationConstraint { + r#type: constraint.r#type, + property: constraint.property, + operator: constraint.operator, + value: constraint.value, + }); + } + + eval_rule.segments.insert( + rule_segment.key.clone(), + flipt_models::EvaluationSegment { + segment_key: rule_segment.key, + match_type: rule_segment.match_type, + constraints: eval_constraints, + }, + ); + } + } + + let mut evaluation_distributions: Vec = + Vec::new(); + + for distribution in rule.distributions { + evaluation_distributions.push(flipt_models::EvaluationDistribution { + rule_id: rule_id.clone(), + variant_key: distribution.variant_key, + variant_attachment: distribution.variant_attachment, + rollout: distribution.rollout, + }) + } + + eval_dists.insert(rule_id.clone(), evaluation_distributions); + + eval_rules_collection.push(eval_rule); + } + + eval_rules.insert(flag.key.clone(), eval_rules_collection); + + // Flag Rollouts + let mut eval_rollout_collection: Vec = Vec::new(); + let mut rollout_idx = 0; + + let flag_rollouts = flag.rollouts.unwrap_or(vec![]); + + for rollout in flag_rollouts { + rollout_idx += 1; + + let mut evaluation_rollout: flipt_models::EvaluationRollout = + flipt_models::EvaluationRollout { + rank: rollout_idx, + rollout_type: common::RolloutType::Unknown, + segment: None, + threshold: None, + }; + + evaluation_rollout.rank = rollout_idx; + + if rollout.threshold.is_some() { + let threshold = rollout.threshold.unwrap(); + evaluation_rollout.threshold = Some(flipt_models::RolloutThreshold { + percentage: threshold.percentage, + value: threshold.value, + }); + + evaluation_rollout.rollout_type = common::RolloutType::Threshold; + } else if rollout.segment.is_some() { + let mut evaluation_rollout_segments: HashMap< + String, + flipt_models::EvaluationSegment, + > = HashMap::new(); + + let segment_rule = rollout.segment.unwrap(); + + for segment in segment_rule.segments { + let mut constraints: Vec = + Vec::new(); + for constraint in segment.constraints { + constraints.push(flipt_models::EvaluationConstraint { + r#type: constraint.r#type, + property: constraint.property, + value: constraint.value, + operator: constraint.operator, + }); + } + + evaluation_rollout_segments.insert( + segment.key.clone(), + flipt_models::EvaluationSegment { + segment_key: segment.key, + match_type: segment.match_type.clone(), + constraints, + }, + ); + } + + evaluation_rollout.rollout_type = common::RolloutType::Segment; + evaluation_rollout.segment = Some(flipt_models::RolloutSegment { + value: segment_rule.value, + segment_operator: segment_rule + .segment_operator + .unwrap_or(common::SegmentOperator::Or), + segments: evaluation_rollout_segments, + }); + } + + eval_rollout_collection.push(evaluation_rollout); + } + + eval_rollouts.insert(flag.key.clone(), eval_rollout_collection); + } + + ns.insert( + n.clone(), + Namespace { + _key: n.clone(), + flags, + eval_rules, + eval_rollouts, + eval_distributions: eval_dists, + }, + ); + } + + Ok(Self { namespace: ns }) + } + + pub fn get_flag(&self, namespace_key: &str, flag_key: &str) -> Option { + let namespace = self.namespace.get(namespace_key)?; + + let flag = namespace.flags.get(flag_key)?; + + Some(flag.clone()) + } + + pub fn get_evaluation_rules( + &self, + namespace_key: &str, + flag_key: &str, + ) -> Option> { + let namespace = self.namespace.get(namespace_key)?; + + let eval_rules = namespace.eval_rules.get(flag_key)?; + + Some(eval_rules.to_vec()) + } + + pub fn get_evaluation_distributions( + &self, + namespace_key: &str, + rule_id: &str, + ) -> Option> { + let namespace = self.namespace.get(namespace_key)?; + + let evaluation_distributions = namespace.eval_distributions.get(rule_id)?; + + Some(evaluation_distributions.to_vec()) + } + + pub fn get_evaluation_rollouts( + &self, + namespace_key: &str, + flag_key: &str, + ) -> Option> { + let namespace = self.namespace.get(namespace_key)?; + + let eval_rollouts = namespace.eval_rollouts.get(flag_key)?; + + Some(eval_rollouts.to_vec()) + } +} + +#[cfg(test)] +mod tests { + use super::Parser; + use super::Snapshot; + use crate::common; + use crate::flipt::models as flipt_models; + use crate::flipt::models::EvaluationConstraint; + use crate::store::parsers::TestParser; + + #[test] + fn snapshot_tests() { + let tp = TestParser::new(vec!["default".into()]); + + let snapshot = match Snapshot::build(&tp) { + Ok(s) => s, + Err(e) => panic!("{}", e), + }; + + let flag_variant = snapshot + .get_flag("default", "flag1") + .expect("flag1 should exist"); + + assert_eq!(flag_variant.key, "flag1"); + assert_eq!(flag_variant.enabled, true); + assert_eq!(flag_variant.r#type, common::FlagType::Variant); + + let flag_boolean = snapshot + .get_flag("default", "flag_boolean") + .expect("flag_boolean should exist"); + + assert_eq!(flag_boolean.key, "flag_boolean"); + assert_eq!(flag_boolean.enabled, true); + assert_eq!(flag_boolean.r#type, common::FlagType::Boolean); + + let evaluation_rules = snapshot + .get_evaluation_rules("default", "flag1") + .expect("evaluation rules should exist for flag1"); + + assert_eq!(evaluation_rules.len(), 1); + assert_eq!(evaluation_rules[0].flag_key, "flag1"); + assert_eq!( + evaluation_rules[0].segment_operator, + common::SegmentOperator::Or + ); + assert_eq!(evaluation_rules[0].rank, 1); + assert_eq!(evaluation_rules[0].segments.len(), 1); + assert_eq!( + *evaluation_rules[0] + .segments + .get("segment1") + .expect("segment1 should exist"), + flipt_models::EvaluationSegment { + segment_key: "segment1".into(), + match_type: common::SegmentMatchType::Any, + constraints: vec![EvaluationConstraint { + r#type: common::ConstraintComparisonType::String, + property: "fizz".into(), + operator: "eq".into(), + value: "buzz".into(), + }], + } + ); + + let evaluation_distributions = snapshot + .get_evaluation_distributions("default", &evaluation_rules[0].id) + .expect("evaluation distributions should exists for the rule"); + assert_eq!(evaluation_distributions.len(), 1); + assert_eq!(evaluation_distributions[0].rollout, 100.0); + assert_eq!(evaluation_distributions[0].rule_id, evaluation_rules[0].id); + assert_eq!(evaluation_distributions[0].variant_key, "variant1"); + + let evaluation_rollouts = snapshot + .get_evaluation_rollouts("default", "flag_boolean") + .expect("evaluation rollouts should exist for flag_boolean"); + + assert_eq!(evaluation_rollouts.len(), 2); + assert_eq!(evaluation_rollouts[0].rank, 1); + assert_eq!( + evaluation_rollouts[0].rollout_type, + common::RolloutType::Segment + ); + + let segment_rollout = evaluation_rollouts[0] + .segment + .as_ref() + .expect("first rollout should be segment"); + + assert_eq!(segment_rollout.value, true); + assert_eq!( + segment_rollout.segment_operator, + common::SegmentOperator::Or + ); + assert_eq!( + *segment_rollout + .segments + .get("segment1") + .expect("segment1 should exist"), + flipt_models::EvaluationSegment { + segment_key: "segment1".into(), + match_type: common::SegmentMatchType::Any, + constraints: vec![EvaluationConstraint { + r#type: common::ConstraintComparisonType::String, + property: "fizz".into(), + operator: "eq".into(), + value: "buzz".into(), + }], + } + ); + } +} diff --git a/client/engine/src/testdata/state.json b/client/engine/src/testdata/state.json new file mode 100644 index 0000000000..9b99e13a01 --- /dev/null +++ b/client/engine/src/testdata/state.json @@ -0,0 +1,105 @@ +{ + "namespace": { + "key": "default", + "name": "", + "description": "", + "protected": false, + "createdAt": null, + "updatedAt": null + }, + "flags": [ + { + "key": "flag1", + "name": "flag1", + "description": "", + "enabled": true, + "type": "VARIANT_FLAG_TYPE", + "createdAt": "2023-11-06T20:12:27.831503Z", + "updatedAt": "2023-11-06T20:12:27.831503Z", + "rules": [ + { + "id": "cd6cf249-2f82-4e68-a126-37fc57e8b515", + "segments": [ + { + "key": "segment1", + "name": "", + "description": "", + "matchType": "ANY_SEGMENT_MATCH_TYPE", + "createdAt": null, + "updatedAt": null, + "constraints": [ + { + "id": "9a189a11-c5ab-4464-98ab-6742dbeab6c0", + "type": "STRING_CONSTRAINT_COMPARISON_TYPE", + "property": "fizz", + "operator": "eq", + "value": "buzz" + } + ] + } + ], + "rank": 1, + "segmentOperator": "OR_SEGMENT_OPERATOR", + "distributions": [ + { + "id": "", + "ruleId": "", + "variantId": "2f76d271-b31e-4f5c-80da-567f9aa1904c", + "variantKey": "variant1", + "variantAttachment": "", + "rollout": 100 + } + ] + } + ], + "rollouts": [] + }, + { + "key": "flag_boolean", + "name": "flag_boolean", + "description": "", + "enabled": true, + "type": "BOOLEAN_FLAG_TYPE", + "createdAt": "2023-11-06T20:12:27.834369Z", + "updatedAt": "2023-11-06T20:12:27.834369Z", + "rules": [], + "rollouts": [ + { + "type": "SEGMENT_ROLLOUT_TYPE", + "rank": 1, + "segment": { + "value": true, + "segmentOperator": "OR_SEGMENT_OPERATOR", + "segments": [ + { + "key": "segment1", + "name": "", + "description": "", + "matchType": "ANY_SEGMENT_MATCH_TYPE", + "createdAt": null, + "updatedAt": null, + "constraints": [ + { + "id": "9a189a11-c5ab-4464-98ab-6742dbeab6c0", + "type": "STRING_CONSTRAINT_COMPARISON_TYPE", + "property": "fizz", + "operator": "eq", + "value": "buzz" + } + ] + } + ] + } + }, + { + "type": "THRESHOLD_ROLLOUT_TYPE", + "rank": 2, + "threshold": { + "percentage": 50, + "value": true + } + } + ] + } + ] +}