From 525685cce4f20f792e05dfeb51b80a168d10c15e Mon Sep 17 00:00:00 2001 From: Timur Gordon <31495328+timurgordon@users.noreply.github.com> Date: Tue, 8 Jul 2025 22:55:47 +0200 Subject: [PATCH] implement payment dsl --- .gitignore | 3 +- Cargo.lock | 449 ++++++++++++- src/client/Cargo.toml | 6 + src/client/cmd/README.md | 157 +++++ src/client/cmd/client.rs | 201 ++++++ src/client/src/lib.rs | 2 +- src/dsl/Cargo.toml | 3 + src/dsl/examples/payment/.env.example | 5 + src/dsl/examples/payment/README.md | 58 ++ src/dsl/examples/payment/main.rs | 46 ++ src/dsl/examples/payment/payment.rhai | 176 +++++ src/dsl/src/circle.rs | 6 + src/dsl/src/lib.rs | 2 + src/dsl/src/payment.rs | 917 ++++++++++++++++++++++++++ src/worker/cmd/README.md | 113 ++++ src/worker/cmd/worker.rs | 20 +- 16 files changed, 2136 insertions(+), 28 deletions(-) create mode 100644 src/client/cmd/README.md create mode 100644 src/client/cmd/client.rs create mode 100644 src/dsl/examples/payment/.env.example create mode 100644 src/dsl/examples/payment/README.md create mode 100644 src/dsl/examples/payment/main.rs create mode 100644 src/dsl/examples/payment/payment.rhai create mode 100644 src/dsl/src/payment.rs create mode 100644 src/worker/cmd/README.md diff --git a/.gitignore b/.gitignore index b6e51e6..7550ebc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ target worker_rhai_temp_db -dump.rdb \ No newline at end of file +dump.rdb +.env \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 6f68ed4..e85c7fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -157,9 +157,9 @@ dependencies = [ "bytes", "futures-util", "http 1.3.1", - "http-body", + "http-body 1.0.1", "http-body-util", - "hyper", + "hyper 1.6.0", "hyper-util", "itoa", "matchit", @@ -172,7 +172,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 1.0.2", "tokio", "tower 0.5.2", "tower-layer", @@ -190,12 +190,12 @@ dependencies = [ "bytes", "futures-util", "http 1.3.1", - "http-body", + "http-body 1.0.1", "http-body-util", "mime", "pin-project-lite", "rustversion", - "sync_wrapper", + "sync_wrapper 1.0.2", "tower-layer", "tower-service", "tracing", @@ -213,9 +213,15 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets", + "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "bincode" version = "1.3.3" @@ -450,6 +456,16 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -643,6 +659,12 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "either" version = "1.15.0" @@ -655,6 +677,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "endian-type" version = "0.1.2" @@ -719,6 +750,21 @@ 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.1" @@ -1195,6 +1241,25 @@ dependencies = [ "syn 2.0.101", ] +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.6.0" @@ -1291,6 +1356,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.1" @@ -1310,7 +1386,7 @@ dependencies = [ "bytes", "futures-core", "http 1.3.1", - "http-body", + "http-body 1.0.1", "pin-project-lite", ] @@ -1338,6 +1414,30 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.6.0" @@ -1348,7 +1448,7 @@ dependencies = [ "futures-channel", "futures-util", "http 1.3.1", - "http-body", + "http-body 1.0.1", "httparse", "httpdate", "itoa", @@ -1357,6 +1457,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.32", + "native-tls", + "tokio", + "tokio-native-tls", +] + [[package]] name = "hyper-util" version = "0.1.14" @@ -1366,8 +1479,8 @@ dependencies = [ "bytes", "futures-core", "http 1.3.1", - "http-body", - "hyper", + "http-body 1.0.1", + "hyper 1.6.0", "pin-project-lite", "tokio", "tower-service", @@ -1543,6 +1656,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + [[package]] name = "is-terminal" version = "0.4.16" @@ -1730,6 +1849,23 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "nibble_vec" version = "0.1.0" @@ -1839,6 +1975,50 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.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.101", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "ourdb" version = "0.1.0" @@ -1875,7 +2055,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -1965,6 +2145,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "plotters" version = "0.3.7" @@ -2261,6 +2447,46 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "rhai" version = "1.21.0" @@ -2309,6 +2535,7 @@ name = "rhai_client" version = "0.1.0" dependencies = [ "chrono", + "clap", "env_logger", "log", "redis", @@ -2368,14 +2595,17 @@ version = "0.1.0" dependencies = [ "chrono", "derive", + "dotenv", "heromodels", "heromodels-derive", "heromodels_core", "macros", + "reqwest", "rhai", "serde", "serde_json", "tempfile", + "tokio", ] [[package]] @@ -2451,6 +2681,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + [[package]] name = "rustversion" version = "1.0.21" @@ -2506,12 +2745,44 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + [[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.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.1", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "serde" version = "1.0.219" @@ -2725,6 +2996,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -2742,6 +3019,27 @@ dependencies = [ "syn 2.0.101", ] +[[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.20.0" @@ -2882,6 +3180,16 @@ dependencies = [ "syn 2.0.101", ] +[[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-stream" version = "0.1.17" @@ -2943,7 +3251,7 @@ dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper", + "sync_wrapper 1.0.2", "tokio", "tower-layer", "tower-service", @@ -2960,7 +3268,7 @@ dependencies = [ "bytes", "futures-util", "http 1.3.1", - "http-body", + "http-body 1.0.1", "http-body-util", "http-range-header", "httpdate", @@ -3049,6 +3357,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "tst" version = "0.1.0" @@ -3147,6 +3461,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -3169,6 +3489,15 @@ dependencies = [ "winapi-util", ] +[[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" @@ -3378,13 +3707,22 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -3393,7 +3731,22 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[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]] @@ -3402,28 +3755,46 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[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" @@ -3436,24 +3807,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[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" @@ -3469,6 +3864,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/src/client/Cargo.toml b/src/client/Cargo.toml index ad24359..4e756f2 100644 --- a/src/client/Cargo.toml +++ b/src/client/Cargo.toml @@ -3,7 +3,13 @@ name = "rhai_client" version = "0.1.0" edition = "2021" +[[bin]] +name = "client" +path = "cmd/client.rs" + [dependencies] +clap = { version = "4.4", features = ["derive"] } +env_logger = "0.10" redis = { version = "0.25.0", features = ["tokio-comp"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/src/client/cmd/README.md b/src/client/cmd/README.md new file mode 100644 index 0000000..89cd1e4 --- /dev/null +++ b/src/client/cmd/README.md @@ -0,0 +1,157 @@ +# Rhai Client Binary + +A command-line client for executing Rhai scripts on remote workers via Redis. + +## Binary: `client` + +### Installation + +Build the binary: +```bash +cargo build --bin client --release +``` + +### Usage + +```bash +# Basic usage - requires caller and circle keys +client --caller-key --circle-key + +# Execute inline script +client -c -k --script "print('Hello World!')" + +# Execute script from file +client -c -k --file script.rhai + +# Use specific worker (defaults to circle key) +client -c -k -w --script "2 + 2" + +# Custom Redis and timeout +client -c -k --redis-url redis://localhost:6379/1 --timeout 60 + +# Remove timestamps from logs +client -c -k --no-timestamp + +# Increase verbosity +client -c -k -v --script "debug_info()" +``` + +### Command-Line Options + +| Option | Short | Default | Description | +|--------|-------|---------|-------------| +| `--caller-key` | `-c` | **Required** | Caller public key (your identity) | +| `--circle-key` | `-k` | **Required** | Circle public key (execution context) | +| `--worker-key` | `-w` | `circle-key` | Worker public key (target worker) | +| `--redis-url` | `-r` | `redis://localhost:6379` | Redis connection URL | +| `--script` | `-s` | | Rhai script to execute | +| `--file` | `-f` | | Path to Rhai script file | +| `--timeout` | `-t` | `30` | Timeout for script execution (seconds) | +| `--no-timestamp` | | `false` | Remove timestamps from log output | +| `--verbose` | `-v` | | Increase verbosity (stackable) | + +### Execution Modes + +#### Inline Script Execution +```bash +# Execute a simple calculation +client -c caller_123 -k circle_456 -s "let result = 2 + 2; print(result);" + +# Execute with specific worker +client -c caller_123 -k circle_456 -w worker_789 -s "get_user_data()" +``` + +#### Script File Execution +```bash +# Execute script from file +client -c caller_123 -k circle_456 -f examples/data_processing.rhai + +# Execute with custom timeout +client -c caller_123 -k circle_456 -f long_running_script.rhai -t 120 +``` + +#### Interactive Mode +```bash +# Enter interactive REPL mode (when no script or file provided) +client -c caller_123 -k circle_456 + +# Interactive mode with verbose logging +client -c caller_123 -k circle_456 -v --no-timestamp +``` + +### Interactive Mode + +When no script (`-s`) or file (`-f`) is provided, the client enters interactive mode: + +``` +šŸ”— Starting Rhai Client +šŸ“‹ Configuration: + Caller Key: caller_123 + Circle Key: circle_456 + Worker Key: circle_456 + Redis URL: redis://localhost:6379 + Timeout: 30s + +āœ… Connected to Redis at redis://localhost:6379 +šŸŽ® Entering interactive mode +Type Rhai scripts and press Enter to execute. Type 'exit' or 'quit' to close. +rhai> let x = 42; print(x); +Status: completed +Output: 42 +rhai> exit +šŸ‘‹ Goodbye! +``` + +### Configuration Examples + +#### Development Usage +```bash +# Simple development client +client -c dev_user -k dev_circle + +# Development with clean logs +client -c dev_user -k dev_circle --no-timestamp -v +``` + +#### Production Usage +```bash +# Production client with specific worker +client \ + --caller-key prod_user_123 \ + --circle-key prod_circle_456 \ + --worker-key prod_worker_789 \ + --redis-url redis://redis-cluster:6379/0 \ + --timeout 300 \ + --file production_script.rhai +``` + +#### Batch Processing +```bash +# Process multiple scripts +for script in scripts/*.rhai; do + client -c batch_user -k batch_circle -f "$script" --no-timestamp +done +``` + +### Key Concepts + +- **Caller Key**: Your identity - used for authentication and tracking +- **Circle Key**: Execution context - defines the environment/permissions +- **Worker Key**: Target worker - which worker should execute the script (defaults to circle key) + +### Error Handling + +The client provides clear error messages for: +- Missing required keys +- Redis connection failures +- Script execution timeouts +- Worker unavailability +- Script syntax errors + +### Dependencies + +- `rhai_client`: Core client library for Redis-based script execution +- `redis`: Redis client for task queue communication +- `clap`: Command-line argument parsing +- `env_logger`: Logging infrastructure +- `tokio`: Async runtime \ No newline at end of file diff --git a/src/client/cmd/client.rs b/src/client/cmd/client.rs new file mode 100644 index 0000000..fe6c3bb --- /dev/null +++ b/src/client/cmd/client.rs @@ -0,0 +1,201 @@ +use clap::Parser; +use rhai_client::{RhaiClient, RhaiClientBuilder}; +use log::{error, info}; +use std::io::{self, Write}; +use std::time::Duration; + +#[derive(Parser, Debug)] +#[command(author, version, about = "Rhai Client - Script execution client", long_about = None)] +struct Args { + /// Caller public key (caller ID) + #[arg(short = 'c', long = "caller-key", help = "Caller public key (your identity)")] + caller_public_key: String, + + /// Circle public key (context ID) + #[arg(short = 'k', long = "circle-key", help = "Circle public key (execution context)")] + circle_public_key: String, + + /// Worker public key (defaults to circle public key if not provided) + #[arg(short = 'w', long = "worker-key", help = "Worker public key (defaults to circle key)")] + worker_public_key: Option, + + /// Redis URL + #[arg(short, long, default_value = "redis://localhost:6379", help = "Redis connection URL")] + redis_url: String, + + /// Rhai script to execute + #[arg(short, long, help = "Rhai script to execute")] + script: Option, + + /// Path to Rhai script file + #[arg(short, long, help = "Path to Rhai script file")] + file: Option, + + /// Timeout for script execution (in seconds) + #[arg(short, long, default_value = "30", help = "Timeout for script execution in seconds")] + timeout: u64, + + /// Increase verbosity (can be used multiple times) + #[arg(short, long, action = clap::ArgAction::Count, help = "Increase verbosity (-v for debug, -vv for trace)")] + verbose: u8, + + /// Disable timestamps in log output + #[arg(long, help = "Remove timestamps from log output")] + no_timestamp: bool, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let args = Args::parse(); + + // Configure logging based on verbosity level + let log_config = match args.verbose { + 0 => "warn,rhai_client=info", + 1 => "info,rhai_client=debug", + 2 => "debug", + _ => "trace", + }; + + std::env::set_var("RUST_LOG", log_config); + + // Configure env_logger with or without timestamps + if args.no_timestamp { + env_logger::Builder::from_default_env() + .format_timestamp(None) + .init(); + } else { + env_logger::init(); + } + + // Use worker key or default to circle key + let worker_key = args.worker_public_key.unwrap_or_else(|| args.circle_public_key.clone()); + + info!("šŸ”— Starting Rhai Client"); + info!("šŸ“‹ Configuration:"); + info!(" Caller Key: {}", args.caller_public_key); + info!(" Circle Key: {}", args.circle_public_key); + info!(" Worker Key: {}", worker_key); + info!(" Redis URL: {}", args.redis_url); + info!(" Timeout: {}s", args.timeout); + info!(""); + + // Create the Rhai client + let client = RhaiClientBuilder::new() + .caller_id(&args.caller_public_key) + .redis_url(&args.redis_url) + .build()?; + + info!("āœ… Connected to Redis at {}", args.redis_url); + + // Determine execution mode + if let Some(script_content) = args.script { + // Execute inline script + info!("šŸ“œ Executing inline script"); + execute_script(&client, &worker_key, script_content, args.timeout).await?; + } else if let Some(file_path) = args.file { + // Execute script from file + info!("šŸ“ Loading script from file: {}", file_path); + let script_content = std::fs::read_to_string(&file_path) + .map_err(|e| format!("Failed to read script file '{}': {}", file_path, e))?; + execute_script(&client, &worker_key, script_content, args.timeout).await?; + } else { + // Interactive mode + info!("šŸŽ® Entering interactive mode"); + info!("Type Rhai scripts and press Enter to execute. Type 'exit' or 'quit' to close."); + run_interactive_mode(&client, &worker_key, args.timeout).await?; + } + + Ok(()) +} + +async fn execute_script( + client: &RhaiClient, + worker_key: &str, + script: String, + timeout_secs: u64, +) -> Result<(), Box> { + info!("⚔ Executing script: {:.50}...", script); + + let timeout = Duration::from_secs(timeout_secs); + + match client + .new_play_request() + .recipient_id(worker_key) + .script(&script) + .timeout(timeout) + .await_response() + .await + { + Ok(result) => { + info!("āœ… Script execution completed"); + println!("Status: {}", result.status); + if let Some(output) = result.output { + println!("Output: {}", output); + } + if let Some(error) = result.error { + println!("Error: {}", error); + } + } + Err(e) => { + error!("āŒ Script execution failed: {}", e); + return Err(Box::new(e)); + } + } + + Ok(()) +} + +async fn run_interactive_mode( + client: &RhaiClient, + worker_key: &str, + timeout_secs: u64, +) -> Result<(), Box> { + let timeout = Duration::from_secs(timeout_secs); + + loop { + print!("rhai> "); + io::stdout().flush()?; + + let mut input = String::new(); + io::stdin().read_line(&mut input)?; + + let input = input.trim(); + + if input.is_empty() { + continue; + } + + if input == "exit" || input == "quit" { + info!("šŸ‘‹ Goodbye!"); + break; + } + + info!("⚔ Executing: {}", input); + + match client + .new_play_request() + .recipient_id(worker_key) + .script(input) + .timeout(timeout) + .await_response() + .await + { + Ok(result) => { + println!("Status: {}", result.status); + if let Some(output) = result.output { + println!("Output: {}", output); + } + if let Some(error) = result.error { + println!("Error: {}", error); + } + } + Err(e) => { + error!("āŒ Execution failed: {}", e); + } + } + + println!(); // Add blank line for readability + } + + Ok(()) +} \ No newline at end of file diff --git a/src/client/src/lib.rs b/src/client/src/lib.rs index bcabf50..dacf965 100644 --- a/src/client/src/lib.rs +++ b/src/client/src/lib.rs @@ -176,7 +176,7 @@ impl<'a> PlayRequestBuilder<'a> { self.request_id.clone() }; // Build the request and submit using self.client - println!("Awaiting response for request {} with timeout {:?}", self.request_id, self.timeout); + info!("Awaiting response for request {} with timeout {:?}", self.request_id, self.timeout); let result = self.client.submit_play_request_and_await_result( &PlayRequest { id: request_id, diff --git a/src/dsl/Cargo.toml b/src/dsl/Cargo.toml index ddb01f3..193d8d6 100644 --- a/src/dsl/Cargo.toml +++ b/src/dsl/Cargo.toml @@ -14,6 +14,9 @@ macros = { path = "../macros"} derive = { path = "../derive"} serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +reqwest = { version = "0.11", features = ["json"] } +tokio = { version = "1", features = ["full"] } +dotenv = "0.15" [dev-dependencies] tempfile = "3" diff --git a/src/dsl/examples/payment/.env.example b/src/dsl/examples/payment/.env.example new file mode 100644 index 0000000..dfed68b --- /dev/null +++ b/src/dsl/examples/payment/.env.example @@ -0,0 +1,5 @@ +# Copy this file to .env and replace with your actual Stripe API keys +# Get your keys from: https://dashboard.stripe.com/apikeys + +# Stripe Secret Key (starts with sk_test_ for test mode or sk_live_ for live mode) +STRIPE_SECRET_KEY=sk_test_your_stripe_secret_key_here \ No newline at end of file diff --git a/src/dsl/examples/payment/README.md b/src/dsl/examples/payment/README.md new file mode 100644 index 0000000..706e177 --- /dev/null +++ b/src/dsl/examples/payment/README.md @@ -0,0 +1,58 @@ +# Payment Example with Stripe Integration + +This example demonstrates how to use the async HTTP API architecture to make real Stripe API calls from Rhai scripts. + +## Setup + +1. **Get Stripe API Keys** + - Sign up at [Stripe Dashboard](https://dashboard.stripe.com) + - Go to [API Keys](https://dashboard.stripe.com/apikeys) + - Copy your **Secret key** (starts with `sk_test_` for test mode) + +2. **Configure Environment** + ```bash + # Copy the example file + cp .env.example .env + + # Edit .env and add your real Stripe secret key + STRIPE_SECRET_KEY=sk_test_your_actual_key_here + ``` + +3. **Run the Example** + ```bash + # From the rhailib root directory + cd src/dsl && cargo run --example payment + ``` + +## What This Example Does + +- **Loads environment variables** from `.env` file +- **Configures async HTTP architecture** with real Stripe API credentials +- **Creates Stripe objects** using the builder pattern: + - Products + - Prices (one-time and recurring) + - Coupons (percentage and fixed amount) + - Payment Intents + - Subscriptions + +## Architecture Features Demonstrated + +- āœ… **Async HTTP calls** from synchronous Rhai scripts +- āœ… **MPSC channel communication** between Rhai and async workers +- āœ… **Environment variable loading** for secure API key management +- āœ… **Error handling** with proper Stripe API error propagation +- āœ… **Builder pattern** for creating complex Stripe objects +- āœ… **Multi-threaded execution** with dedicated async worker threads + +## Expected Output + +With a valid Stripe API key, you'll see: +``` +šŸ”§ Configuring async HTTP client with timeouts... +šŸš€ Async worker thread started +šŸ”„ Processing POST request to products +šŸ“„ Stripe response: {"id":"prod_...","object":"product",...} +āœ… Product created successfully with ID: prod_... +``` + +Without a valid key, you'll see the demo behavior with error handling. \ No newline at end of file diff --git a/src/dsl/examples/payment/main.rs b/src/dsl/examples/payment/main.rs new file mode 100644 index 0000000..0483f9e --- /dev/null +++ b/src/dsl/examples/payment/main.rs @@ -0,0 +1,46 @@ +use rhailib_dsl::payment::register_payment_rhai_module; +use rhai::{Engine, EvalAltResult, Scope}; +use std::fs; +use std::env; + +fn main() -> Result<(), Box> { + // Load environment variables from .env file + dotenv::from_filename("examples/payment/.env").ok(); + + // Get Stripe API key from environment + let stripe_secret_key = env::var("STRIPE_SECRET_KEY") + .unwrap_or_else(|_| { + println!("āš ļø STRIPE_SECRET_KEY not found in .env file, using demo key"); + println!(" Create examples/payment/.env with: STRIPE_SECRET_KEY=sk_test_your_key_here"); + "sk_test_demo_key_will_fail_gracefully".to_string() + }); + + // Create a new Rhai engine + let mut engine = Engine::new(); + + // Register the payment module + register_payment_rhai_module(&mut engine); + + // Create a scope and set the Stripe API key variable + let mut scope = Scope::new(); + scope.push("STRIPE_API_KEY", stripe_secret_key.clone()); + + println!("=== Rhai Payment Module Example ==="); + println!("šŸ”‘ Using Stripe API key: {}***", &stripe_secret_key[..15.min(stripe_secret_key.len())]); + println!("Reading and executing payment.rhai script...\n"); + + // Read the Rhai script + let script = fs::read_to_string("examples/payment/payment.rhai") + .expect("Failed to read payment.rhai file"); + + // Execute the script with the scope + match engine.eval_with_scope::<()>(&mut scope, &script) { + Ok(_) => println!("\nāœ… Payment script executed successfully!"), + Err(e) => { + eprintln!("āŒ Error executing script: {}", e); + return Err(e); + } + } + + Ok(()) +} \ No newline at end of file diff --git a/src/dsl/examples/payment/payment.rhai b/src/dsl/examples/payment/payment.rhai new file mode 100644 index 0000000..11ad109 --- /dev/null +++ b/src/dsl/examples/payment/payment.rhai @@ -0,0 +1,176 @@ +// ===== Stripe Payment Integration Example ===== +// This script demonstrates the complete payment workflow using Stripe + +print("šŸ”§ Configuring Stripe..."); +// Configure Stripe with API key from environment variables +// The STRIPE_API_KEY is loaded from .env file by main.rs +let config_result = configure_stripe(STRIPE_API_KEY); +print(`Configuration result: ${config_result}`); + +print("\nšŸ“¦ Creating a Product..."); +// Create a new product using builder pattern +let product = new_product() + .name("Premium Software License") + .description("A comprehensive software solution for businesses") + .metadata("category", "software") + .metadata("tier", "premium"); + +print(`Product created: ${product.name}`); + +// Create the product in Stripe +print("šŸ”„ Attempting to create product in Stripe..."); +try { + let product_id = product.create(); + print(`āœ… Product ID: ${product_id}`); +} catch(error) { + print(`āŒ Failed to create product: ${error}`); + print("This is expected with a demo API key. In production, use a valid Stripe secret key."); + return; // Exit early since we can't continue without a valid product +} + +print("\nšŸ’° Creating Prices..."); + +// Create upfront price (one-time payment) +let upfront_price = new_price() + .amount(19999) // $199.99 in cents + .currency("usd") + .product(product_id) + .metadata("type", "upfront"); + +let upfront_price_id = upfront_price.create(); +print(`āœ… Upfront Price ID: ${upfront_price_id}`); + +// Create monthly subscription price +let monthly_price = new_price() + .amount(2999) // $29.99 in cents + .currency("usd") + .product(product_id) + .recurring("month") + .metadata("type", "monthly_subscription"); + +let monthly_price_id = monthly_price.create(); +print(`āœ… Monthly Price ID: ${monthly_price_id}`); + +// Create annual subscription price with discount +let annual_price = new_price() + .amount(29999) // $299.99 in cents (2 months free) + .currency("usd") + .product(product_id) + .recurring("year") + .metadata("type", "annual_subscription") + .metadata("discount", "2_months_free"); + +let annual_price_id = annual_price.create(); +print(`āœ… Annual Price ID: ${annual_price_id}`); + +print("\nšŸŽŸļø Creating Discount Coupons..."); + +// Create a percentage-based coupon +let percent_coupon = new_coupon() + .duration("once") + .percent_off(25) + .metadata("campaign", "new_customer_discount") + .metadata("code", "WELCOME25"); + +let percent_coupon_id = percent_coupon.create(); +print(`āœ… 25% Off Coupon ID: ${percent_coupon_id}`); + +// Create a fixed amount coupon +let amount_coupon = new_coupon() + .duration("repeating") + .duration_in_months(3) + .amount_off(500, "usd") // $5.00 off + .metadata("campaign", "loyalty_program") + .metadata("code", "LOYAL5"); + +let amount_coupon_id = amount_coupon.create(); +print(`āœ… $5 Off Coupon ID: ${amount_coupon_id}`); + +print("\nšŸ’³ Creating Payment Intent for Upfront Payment..."); + +// Create a payment intent for one-time payment +let payment_intent = new_payment_intent() + .amount(19999) + .currency("usd") + .customer("cus_example_customer_id") + .description("Premium Software License - One-time Payment") + .add_payment_method_type("card") + .add_payment_method_type("us_bank_account") + .metadata("product_id", product_id) + .metadata("price_id", upfront_price_id) + .metadata("payment_type", "upfront"); + +let payment_intent_id = payment_intent.create(); +print(`āœ… Payment Intent ID: ${payment_intent_id}`); + +print("\nšŸ”„ Creating Subscription..."); + +// Create a subscription for monthly billing +let subscription = new_subscription() + .customer("cus_example_customer_id") + .add_price(monthly_price_id) + .trial_days(14) // 14-day free trial + .coupon(percent_coupon_id) // Apply 25% discount + .metadata("plan", "monthly") + .metadata("trial", "14_days") + .metadata("source", "website_signup"); + +let subscription_id = subscription.create(); +print(`āœ… Subscription ID: ${subscription_id}`); + +print("\nšŸŽÆ Creating Multi-Item Subscription..."); + +// Create a subscription with multiple items +let multi_subscription = new_subscription() + .customer("cus_example_enterprise_customer") + .add_price_with_quantity(monthly_price_id, 5) // 5 licenses + .add_price("price_addon_support_monthly") // Support addon + .trial_days(30) // 30-day trial for enterprise + .metadata("plan", "enterprise") + .metadata("licenses", "5") + .metadata("addons", "premium_support"); + +let multi_subscription_id = multi_subscription.create(); +print(`āœ… Multi-Item Subscription ID: ${multi_subscription_id}`); + +print("\nšŸ’° Creating Payment Intent with Coupon..."); + +// Create another payment intent with discount applied +let discounted_payment = new_payment_intent() + .amount(14999) // Discounted amount after coupon + .currency("usd") + .customer("cus_example_customer_2") + .description("Premium Software License - With 25% Discount") + .metadata("original_amount", "19999") + .metadata("coupon_applied", percent_coupon_id) + .metadata("discount_percent", "25"); + +let discounted_payment_id = discounted_payment.create(); +print(`āœ… Discounted Payment Intent ID: ${discounted_payment_id}`); + +print("\nšŸ“Š Summary of Created Items:"); +print("================================"); +print(`Product ID: ${product_id}`); +print(`Upfront Price ID: ${upfront_price_id}`); +print(`Monthly Price ID: ${monthly_price_id}`); +print(`Annual Price ID: ${annual_price_id}`); +print(`25% Coupon ID: ${percent_coupon_id}`); +print(`$5 Coupon ID: ${amount_coupon_id}`); +print(`Payment Intent ID: ${payment_intent_id}`); +print(`Subscription ID: ${subscription_id}`); +print(`Multi-Subscription ID: ${multi_subscription_id}`); +print(`Discounted Payment ID: ${discounted_payment_id}`); + +print("\nšŸŽ‰ Payment workflow demonstration completed!"); +print("All Stripe objects have been created successfully using the builder pattern."); + +// Example of accessing object properties +print("\nšŸ” Accessing Object Properties:"); +print(`Product Name: ${product.name}`); +print(`Product Description: ${product.description}`); +print(`Upfront Price Amount: $${upfront_price.amount / 100}`); +print(`Monthly Price Currency: ${monthly_price.currency}`); +print(`Subscription Customer: ${subscription.customer}`); +print(`Payment Intent Amount: $${payment_intent.amount / 100}`); +print(`Percent Coupon Duration: ${percent_coupon.duration}`); +print(`Percent Coupon Discount: ${percent_coupon.percent_off}%`); \ No newline at end of file diff --git a/src/dsl/src/circle.rs b/src/dsl/src/circle.rs index 62d572d..1122fb2 100644 --- a/src/dsl/src/circle.rs +++ b/src/dsl/src/circle.rs @@ -15,6 +15,12 @@ use heromodels::models::circle::ThemeData; mod rhai_circle_module { use super::{RhaiCircle}; + // this one configures the users own circle + #[rhai_fn(name = "configure", return_raw)] + pub fn configure() -> Result> { + Ok(Circle::new()) + } + #[rhai_fn(name = "new_circle", return_raw)] pub fn new_circle() -> Result> { Ok(Circle::new()) diff --git a/src/dsl/src/lib.rs b/src/dsl/src/lib.rs index 13f0de0..7627b3d 100644 --- a/src/dsl/src/lib.rs +++ b/src/dsl/src/lib.rs @@ -10,6 +10,7 @@ pub mod finance; pub mod flow; pub mod library; pub mod object; +pub mod payment; pub use macros::register_authorized_get_by_id_fn; pub use macros::register_authorized_list_fn; @@ -28,5 +29,6 @@ pub fn register_dsl_modules(engine: &mut Engine) { flow::register_flow_rhai_modules(engine); library::register_library_rhai_module(engine); object::register_object_fns(engine); + payment::register_payment_rhai_module(engine); println!("Rhailib Domain Specific Language modules registered successfully."); } \ No newline at end of file diff --git a/src/dsl/src/payment.rs b/src/dsl/src/payment.rs new file mode 100644 index 0000000..32ca39f --- /dev/null +++ b/src/dsl/src/payment.rs @@ -0,0 +1,917 @@ +use rhai::plugin::*; +use rhai::{Dynamic, Engine, EvalAltResult, Module}; +use serde::{Serialize, Deserialize}; +use std::collections::HashMap; +use std::mem; +use std::sync::Mutex; +use std::sync::mpsc::{self, Receiver, Sender}; +use std::thread; +use std::time::Duration; +use reqwest::Client; +use tokio::runtime::Runtime; +use tokio::sync::oneshot; + +// Async Function Registry for HTTP API calls +static ASYNC_REGISTRY: Mutex> = Mutex::new(None); + +const STRIPE_API_BASE: &str = "https://api.stripe.com/v1"; + +#[derive(Debug, Clone)] +pub struct AsyncFunctionRegistry { + pub request_sender: Sender, + pub stripe_config: StripeConfig, +} + +#[derive(Debug, Clone)] +pub struct StripeConfig { + pub secret_key: String, + pub client: Client, +} + +#[derive(Debug)] +pub struct AsyncRequest { + pub endpoint: String, + pub method: String, + pub data: HashMap, + pub response_sender: oneshot::Sender>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ApiResponse { + pub id: Option, + pub status: Option, + pub error: Option, +} + +#[derive(Debug, Clone, Serialize, Default)] +pub struct RhaiProduct { + pub id: Option, + pub name: String, + pub description: Option, + pub metadata: HashMap, +} + +#[derive(Debug, Clone, Serialize, Default)] +pub struct RhaiPrice { + pub id: Option, + pub unit_amount: u64, + pub currency: String, + pub recurring: Option, + pub product: String, + pub metadata: HashMap, +} + +#[derive(Debug, Clone, Serialize, Default)] +pub struct RecurringConfig { + pub interval: String, // "month", "year", "week", "day" + pub interval_count: Option, +} + +#[derive(Debug, Clone, Serialize, Default)] +pub struct RhaiSubscription { + pub id: Option, + pub customer: String, + pub items: Vec, + pub metadata: HashMap, + pub trial_period_days: Option, + pub coupon: Option, +} + +#[derive(Debug, Clone, Serialize, Default)] +pub struct SubscriptionItem { + pub price: String, + pub quantity: Option, +} + +#[derive(Debug, Clone, Serialize, Default)] +pub struct RhaiPaymentIntent { + pub id: Option, + pub amount: u64, + pub currency: String, + pub payment_method_types: Vec, + pub customer: Option, + pub description: Option, + pub metadata: HashMap, +} + +#[derive(Debug, Clone, Serialize, Default)] +pub struct RhaiCoupon { + pub id: Option, + pub duration: String, // "once", "repeating", "forever" + pub percent_off: Option, + pub amount_off: Option, + pub currency: Option, + pub duration_in_months: Option, + pub metadata: HashMap, +} + +impl RhaiProduct { + pub fn new() -> Self { + Self { + id: None, + name: String::new(), + description: None, + metadata: HashMap::new(), + } + } + + pub fn name(mut self, name: String) -> Self { + self.name = name; + self + } + + pub fn description(mut self, description: String) -> Self { + self.description = Some(description); + self + } + + pub fn metadata(mut self, key: String, value: String) -> Self { + self.metadata.insert(key, value); + self + } +} + +impl RhaiPrice { + pub fn new() -> Self { + Self { + id: None, + unit_amount: 0, + currency: "usd".to_string(), + recurring: None, + product: String::new(), + metadata: HashMap::new(), + } + } + + pub fn amount(mut self, amount: u64) -> Self { + self.unit_amount = amount; + self + } + + pub fn currency(mut self, currency: String) -> Self { + self.currency = currency; + self + } + + pub fn product(mut self, product_id: String) -> Self { + self.product = product_id; + self + } + + pub fn recurring(mut self, interval: String) -> Self { + self.recurring = Some(RecurringConfig { + interval, + interval_count: None, + }); + self + } + + pub fn recurring_with_count(mut self, interval: String, count: u32) -> Self { + self.recurring = Some(RecurringConfig { + interval, + interval_count: Some(count), + }); + self + } + + pub fn metadata(mut self, key: String, value: String) -> Self { + self.metadata.insert(key, value); + self + } +} + +impl RhaiSubscription { + pub fn new() -> Self { + Self { + id: None, + customer: String::new(), + items: Vec::new(), + metadata: HashMap::new(), + trial_period_days: None, + coupon: None, + } + } + + pub fn customer(mut self, customer_id: String) -> Self { + self.customer = customer_id; + self + } + + pub fn add_price(mut self, price_id: String) -> Self { + self.items.push(SubscriptionItem { + price: price_id, + quantity: None, + }); + self + } + + pub fn add_price_with_quantity(mut self, price_id: String, quantity: u32) -> Self { + self.items.push(SubscriptionItem { + price: price_id, + quantity: Some(quantity), + }); + self + } + + pub fn trial_days(mut self, days: u32) -> Self { + self.trial_period_days = Some(days); + self + } + + pub fn coupon(mut self, coupon_id: String) -> Self { + self.coupon = Some(coupon_id); + self + } + + pub fn metadata(mut self, key: String, value: String) -> Self { + self.metadata.insert(key, value); + self + } +} + +impl RhaiPaymentIntent { + pub fn new() -> Self { + Self { + id: None, + amount: 0, + currency: "usd".to_string(), + payment_method_types: vec!["card".to_string()], + customer: None, + description: None, + metadata: HashMap::new(), + } + } + + pub fn amount(mut self, amount: u64) -> Self { + self.amount = amount; + self + } + + pub fn currency(mut self, currency: String) -> Self { + self.currency = currency; + self + } + + pub fn customer(mut self, customer_id: String) -> Self { + self.customer = Some(customer_id); + self + } + + pub fn description(mut self, description: String) -> Self { + self.description = Some(description); + self + } + + pub fn add_payment_method_type(mut self, method_type: String) -> Self { + if !self.payment_method_types.contains(&method_type) { + self.payment_method_types.push(method_type); + } + self + } + + pub fn metadata(mut self, key: String, value: String) -> Self { + self.metadata.insert(key, value); + self + } +} + +impl RhaiCoupon { + pub fn new() -> Self { + Self { + id: None, + duration: "once".to_string(), + percent_off: None, + amount_off: None, + currency: None, + duration_in_months: None, + metadata: HashMap::new(), + } + } + + pub fn duration(mut self, duration: String) -> Self { + self.duration = duration; + self + } + + pub fn percent_off(mut self, percent: u32) -> Self { + self.percent_off = Some(percent); + self.amount_off = None; // Clear amount_off if setting percent_off + self + } + + pub fn amount_off(mut self, amount: u64, currency: String) -> Self { + self.amount_off = Some(amount); + self.currency = Some(currency); + self.percent_off = None; // Clear percent_off if setting amount_off + self + } + + pub fn duration_in_months(mut self, months: u32) -> Self { + self.duration_in_months = Some(months); + self + } + + pub fn metadata(mut self, key: String, value: String) -> Self { + self.metadata.insert(key, value); + self + } +} + +// Async Worker Pool Implementation +impl AsyncFunctionRegistry { + pub fn new(stripe_config: StripeConfig) -> Self { + let (request_sender, request_receiver) = mpsc::channel(); + + // Start the async worker thread + let config_clone = stripe_config.clone(); + thread::spawn(move || { + let rt = Runtime::new().expect("Failed to create Tokio runtime"); + rt.block_on(async { + Self::async_worker_loop(config_clone, request_receiver).await; + }); + }); + + Self { + request_sender, + stripe_config, + } + } + + async fn async_worker_loop(config: StripeConfig, receiver: Receiver) { + println!("šŸš€ Async worker thread started"); + + while let Ok(request) = receiver.recv() { + let result = Self::handle_stripe_request(&config, &request).await; + let _ = request.response_sender.send(result); + } + } + + async fn handle_stripe_request(config: &StripeConfig, request: &AsyncRequest) -> Result { + println!("šŸ”„ Processing {} request to {}", request.method, request.endpoint); + + let url = format!("{}/{}", STRIPE_API_BASE, request.endpoint); + + let response = config.client + .post(&url) + .basic_auth(&config.secret_key, None::<&str>) + .form(&request.data) + .send() + .await + .map_err(|e| { + println!("āŒ HTTP request failed: {}", e); + format!("HTTP request failed: {}", e) + })?; + + let response_text = response.text().await + .map_err(|e| format!("Failed to read response: {}", e))?; + + println!("šŸ“„ Stripe response: {}", response_text); + + let json: serde_json::Value = serde_json::from_str(&response_text) + .map_err(|e| format!("Failed to parse JSON: {}", e))?; + + if let Some(id) = json.get("id").and_then(|v| v.as_str()) { + println!("āœ… Request successful with ID: {}", id); + Ok(id.to_string()) + } else if let Some(error) = json.get("error") { + let error_msg = format!("Stripe API error: {}", error); + println!("āŒ {}", error_msg); + Err(error_msg) + } else { + let error_msg = format!("Unexpected response: {}", response_text); + println!("āŒ {}", error_msg); + Err(error_msg) + } + } + + pub fn make_request(&self, endpoint: String, method: String, data: HashMap) -> Result { + let (response_sender, response_receiver) = oneshot::channel(); + + let request = AsyncRequest { + endpoint, + method, + data, + response_sender, + }; + + self.request_sender.send(request) + .map_err(|_| "Failed to send request to async worker".to_string())?; + + // Block until we get a response + response_receiver.blocking_recv() + .map_err(|_| "Failed to receive response from async worker".to_string())? + } +} + +// Helper functions to prepare form data for different Stripe objects +fn prepare_product_data(product: &RhaiProduct) -> HashMap { + let mut form_data = HashMap::new(); + form_data.insert("name".to_string(), product.name.clone()); + + if let Some(ref description) = product.description { + form_data.insert("description".to_string(), description.clone()); + } + + for (key, value) in &product.metadata { + let metadata_key = format!("metadata[{}]", key); + form_data.insert(metadata_key, value.clone()); + } + + form_data +} + +fn prepare_price_data(price: &RhaiPrice) -> HashMap { + let mut form_data = HashMap::new(); + form_data.insert("unit_amount".to_string(), price.unit_amount.to_string()); + form_data.insert("currency".to_string(), price.currency.clone()); + form_data.insert("product".to_string(), price.product.clone()); + + if let Some(ref recurring) = price.recurring { + form_data.insert("recurring[interval]".to_string(), recurring.interval.clone()); + if let Some(count) = recurring.interval_count { + form_data.insert("recurring[interval_count]".to_string(), count.to_string()); + } + } + + for (key, value) in &price.metadata { + let metadata_key = format!("metadata[{}]", key); + form_data.insert(metadata_key, value.clone()); + } + + form_data +} + +fn prepare_subscription_data(subscription: &RhaiSubscription) -> HashMap { + let mut form_data = HashMap::new(); + form_data.insert("customer".to_string(), subscription.customer.clone()); + + for (i, item) in subscription.items.iter().enumerate() { + form_data.insert(format!("items[{}][price]", i), item.price.clone()); + if let Some(quantity) = item.quantity { + form_data.insert(format!("items[{}][quantity]", i), quantity.to_string()); + } + } + + if let Some(trial_days) = subscription.trial_period_days { + form_data.insert("trial_period_days".to_string(), trial_days.to_string()); + } + + if let Some(ref coupon) = subscription.coupon { + form_data.insert("coupon".to_string(), coupon.clone()); + } + + for (key, value) in &subscription.metadata { + form_data.insert(format!("metadata[{}]", key), value.clone()); + } + + form_data +} + +fn prepare_payment_intent_data(intent: &RhaiPaymentIntent) -> HashMap { + let mut form_data = HashMap::new(); + form_data.insert("amount".to_string(), intent.amount.to_string()); + form_data.insert("currency".to_string(), intent.currency.clone()); + + for (i, method_type) in intent.payment_method_types.iter().enumerate() { + form_data.insert(format!("payment_method_types[{}]", i), method_type.clone()); + } + + if let Some(ref customer) = intent.customer { + form_data.insert("customer".to_string(), customer.clone()); + } + + if let Some(ref description) = intent.description { + form_data.insert("description".to_string(), description.clone()); + } + + for (key, value) in &intent.metadata { + form_data.insert(format!("metadata[{}]", key), value.clone()); + } + + form_data +} + +fn prepare_coupon_data(coupon: &RhaiCoupon) -> HashMap { + let mut form_data = HashMap::new(); + form_data.insert("duration".to_string(), coupon.duration.clone()); + + if let Some(percent) = coupon.percent_off { + form_data.insert("percent_off".to_string(), percent.to_string()); + } + + if let Some(amount) = coupon.amount_off { + form_data.insert("amount_off".to_string(), amount.to_string()); + if let Some(ref currency) = coupon.currency { + form_data.insert("currency".to_string(), currency.clone()); + } + } + + if let Some(months) = coupon.duration_in_months { + form_data.insert("duration_in_months".to_string(), months.to_string()); + } + + for (key, value) in &coupon.metadata { + form_data.insert(format!("metadata[{}]", key), value.clone()); + } + + form_data +} + +#[export_module] +mod rhai_payment_module { + use super::*; + + // --- Configuration --- + #[rhai_fn(name = "configure_stripe", return_raw)] + pub fn configure_stripe(secret_key: String) -> Result> { + println!("šŸ”§ Configuring async HTTP client with timeouts..."); + + let client = Client::builder() + .timeout(Duration::from_secs(5)) + .connect_timeout(Duration::from_secs(3)) + .pool_idle_timeout(Duration::from_secs(10)) + .tcp_keepalive(Duration::from_secs(30)) + .user_agent("rhailib-payment/1.0") + .build() + .map_err(|e| format!("Failed to create HTTP client: {}", e))?; + + let stripe_config = StripeConfig { + secret_key, + client, + }; + + let registry = AsyncFunctionRegistry::new(stripe_config); + + let mut global_registry = ASYNC_REGISTRY.lock().unwrap(); + *global_registry = Some(registry); + + Ok("Stripe configured successfully with async architecture".to_string()) + } + + // --- Product Builder --- + #[rhai_fn(name = "new_product", return_raw)] + pub fn new_product() -> Result> { + Ok(RhaiProduct::new()) + } + + #[rhai_fn(name = "name", return_raw)] + pub fn product_name(product: &mut RhaiProduct, name: String) -> Result> { + let owned = mem::take(product); + *product = owned.name(name); + Ok(product.clone()) + } + + #[rhai_fn(name = "description", return_raw)] + pub fn product_description(product: &mut RhaiProduct, description: String) -> Result> { + let owned = mem::take(product); + *product = owned.description(description); + Ok(product.clone()) + } + + #[rhai_fn(name = "metadata", return_raw)] + pub fn product_metadata(product: &mut RhaiProduct, key: String, value: String) -> Result> { + let owned = mem::take(product); + *product = owned.metadata(key, value); + Ok(product.clone()) + } + + #[rhai_fn(name = "create", return_raw)] + pub fn create_product(product: &mut RhaiProduct) -> Result> { + let registry = ASYNC_REGISTRY.lock().unwrap(); + let registry = registry.as_ref().ok_or("Stripe not configured. Call configure_stripe() first.")?; + + let form_data = prepare_product_data(product); + let result = registry.make_request("products".to_string(), "POST".to_string(), form_data) + .map_err(|e| e.to_string())?; + + product.id = Some(result.clone()); + Ok(result) + } + + // --- Price Builder --- + #[rhai_fn(name = "new_price", return_raw)] + pub fn new_price() -> Result> { + Ok(RhaiPrice::new()) + } + + #[rhai_fn(name = "amount", return_raw)] + pub fn price_amount(price: &mut RhaiPrice, amount: i64) -> Result> { + let owned = mem::take(price); + *price = owned.amount(amount as u64); + Ok(price.clone()) + } + + #[rhai_fn(name = "currency", return_raw)] + pub fn price_currency(price: &mut RhaiPrice, currency: String) -> Result> { + let owned = mem::take(price); + *price = owned.currency(currency); + Ok(price.clone()) + } + + #[rhai_fn(name = "product", return_raw)] + pub fn price_product(price: &mut RhaiPrice, product_id: String) -> Result> { + let owned = mem::take(price); + *price = owned.product(product_id); + Ok(price.clone()) + } + + #[rhai_fn(name = "recurring", return_raw)] + pub fn price_recurring(price: &mut RhaiPrice, interval: String) -> Result> { + let owned = mem::take(price); + *price = owned.recurring(interval); + Ok(price.clone()) + } + + #[rhai_fn(name = "recurring_with_count", return_raw)] + pub fn price_recurring_with_count(price: &mut RhaiPrice, interval: String, count: i64) -> Result> { + let owned = mem::take(price); + *price = owned.recurring_with_count(interval, count as u32); + Ok(price.clone()) + } + + #[rhai_fn(name = "metadata", return_raw)] + pub fn price_metadata(price: &mut RhaiPrice, key: String, value: String) -> Result> { + let owned = mem::take(price); + *price = owned.metadata(key, value); + Ok(price.clone()) + } + + #[rhai_fn(name = "create", return_raw)] + pub fn create_price(price: &mut RhaiPrice) -> Result> { + let registry = ASYNC_REGISTRY.lock().unwrap(); + let registry = registry.as_ref().ok_or("Stripe not configured. Call configure_stripe() first.")?; + + let form_data = prepare_price_data(price); + let result = registry.make_request("prices".to_string(), "POST".to_string(), form_data) + .map_err(|e| e.to_string())?; + + price.id = Some(result.clone()); + Ok(result) + } + + // --- Subscription Builder --- + #[rhai_fn(name = "new_subscription", return_raw)] + pub fn new_subscription() -> Result> { + Ok(RhaiSubscription::new()) + } + + #[rhai_fn(name = "customer", return_raw)] + pub fn subscription_customer(subscription: &mut RhaiSubscription, customer_id: String) -> Result> { + let owned = mem::take(subscription); + *subscription = owned.customer(customer_id); + Ok(subscription.clone()) + } + + #[rhai_fn(name = "add_price", return_raw)] + pub fn subscription_add_price(subscription: &mut RhaiSubscription, price_id: String) -> Result> { + let owned = mem::take(subscription); + *subscription = owned.add_price(price_id); + Ok(subscription.clone()) + } + + #[rhai_fn(name = "add_price_with_quantity", return_raw)] + pub fn subscription_add_price_with_quantity(subscription: &mut RhaiSubscription, price_id: String, quantity: i64) -> Result> { + let owned = mem::take(subscription); + *subscription = owned.add_price_with_quantity(price_id, quantity as u32); + Ok(subscription.clone()) + } + + #[rhai_fn(name = "trial_days", return_raw)] + pub fn subscription_trial_days(subscription: &mut RhaiSubscription, days: i64) -> Result> { + let owned = mem::take(subscription); + *subscription = owned.trial_days(days as u32); + Ok(subscription.clone()) + } + + #[rhai_fn(name = "coupon", return_raw)] + pub fn subscription_coupon(subscription: &mut RhaiSubscription, coupon_id: String) -> Result> { + let owned = mem::take(subscription); + *subscription = owned.coupon(coupon_id); + Ok(subscription.clone()) + } + + #[rhai_fn(name = "metadata", return_raw)] + pub fn subscription_metadata(subscription: &mut RhaiSubscription, key: String, value: String) -> Result> { + let owned = mem::take(subscription); + *subscription = owned.metadata(key, value); + Ok(subscription.clone()) + } + + #[rhai_fn(name = "create", return_raw)] + pub fn create_subscription(subscription: &mut RhaiSubscription) -> Result> { + let registry = ASYNC_REGISTRY.lock().unwrap(); + let registry = registry.as_ref().ok_or("Stripe not configured. Call configure_stripe() first.")?; + + let form_data = prepare_subscription_data(subscription); + let result = registry.make_request("subscriptions".to_string(), "POST".to_string(), form_data) + .map_err(|e| e.to_string())?; + + subscription.id = Some(result.clone()); + Ok(result) + } + + // --- Payment Intent Builder --- + #[rhai_fn(name = "new_payment_intent", return_raw)] + pub fn new_payment_intent() -> Result> { + Ok(RhaiPaymentIntent::new()) + } + + #[rhai_fn(name = "amount", return_raw)] + pub fn payment_intent_amount(intent: &mut RhaiPaymentIntent, amount: i64) -> Result> { + let owned = mem::take(intent); + *intent = owned.amount(amount as u64); + Ok(intent.clone()) + } + + #[rhai_fn(name = "currency", return_raw)] + pub fn payment_intent_currency(intent: &mut RhaiPaymentIntent, currency: String) -> Result> { + let owned = mem::take(intent); + *intent = owned.currency(currency); + Ok(intent.clone()) + } + + #[rhai_fn(name = "customer", return_raw)] + pub fn payment_intent_customer(intent: &mut RhaiPaymentIntent, customer_id: String) -> Result> { + let owned = mem::take(intent); + *intent = owned.customer(customer_id); + Ok(intent.clone()) + } + + #[rhai_fn(name = "description", return_raw)] + pub fn payment_intent_description(intent: &mut RhaiPaymentIntent, description: String) -> Result> { + let owned = mem::take(intent); + *intent = owned.description(description); + Ok(intent.clone()) + } + + #[rhai_fn(name = "add_payment_method_type", return_raw)] + pub fn payment_intent_add_payment_method_type(intent: &mut RhaiPaymentIntent, method_type: String) -> Result> { + let owned = mem::take(intent); + *intent = owned.add_payment_method_type(method_type); + Ok(intent.clone()) + } + + #[rhai_fn(name = "metadata", return_raw)] + pub fn payment_intent_metadata(intent: &mut RhaiPaymentIntent, key: String, value: String) -> Result> { + let owned = mem::take(intent); + *intent = owned.metadata(key, value); + Ok(intent.clone()) + } + + #[rhai_fn(name = "create", return_raw)] + pub fn create_payment_intent(intent: &mut RhaiPaymentIntent) -> Result> { + let registry = ASYNC_REGISTRY.lock().unwrap(); + let registry = registry.as_ref().ok_or("Stripe not configured. Call configure_stripe() first.")?; + + let form_data = prepare_payment_intent_data(intent); + let result = registry.make_request("payment_intents".to_string(), "POST".to_string(), form_data) + .map_err(|e| e.to_string())?; + + intent.id = Some(result.clone()); + Ok(result) + } + + // --- Coupon Builder --- + #[rhai_fn(name = "new_coupon", return_raw)] + pub fn new_coupon() -> Result> { + Ok(RhaiCoupon::new()) + } + + #[rhai_fn(name = "duration", return_raw)] + pub fn coupon_duration(coupon: &mut RhaiCoupon, duration: String) -> Result> { + let owned = mem::take(coupon); + *coupon = owned.duration(duration); + Ok(coupon.clone()) + } + + #[rhai_fn(name = "percent_off", return_raw)] + pub fn coupon_percent_off(coupon: &mut RhaiCoupon, percent: i64) -> Result> { + let owned = mem::take(coupon); + *coupon = owned.percent_off(percent as u32); + Ok(coupon.clone()) + } + + #[rhai_fn(name = "amount_off", return_raw)] + pub fn coupon_amount_off(coupon: &mut RhaiCoupon, amount: i64, currency: String) -> Result> { + let owned = mem::take(coupon); + *coupon = owned.amount_off(amount as u64, currency); + Ok(coupon.clone()) + } + + #[rhai_fn(name = "duration_in_months", return_raw)] + pub fn coupon_duration_in_months(coupon: &mut RhaiCoupon, months: i64) -> Result> { + let owned = mem::take(coupon); + *coupon = owned.duration_in_months(months as u32); + Ok(coupon.clone()) + } + + #[rhai_fn(name = "metadata", return_raw)] + pub fn coupon_metadata(coupon: &mut RhaiCoupon, key: String, value: String) -> Result> { + let owned = mem::take(coupon); + *coupon = owned.metadata(key, value); + Ok(coupon.clone()) + } + + #[rhai_fn(name = "create", return_raw)] + pub fn create_coupon(coupon: &mut RhaiCoupon) -> Result> { + let registry = ASYNC_REGISTRY.lock().unwrap(); + let registry = registry.as_ref().ok_or("Stripe not configured. Call configure_stripe() first.")?; + + let form_data = prepare_coupon_data(coupon); + let result = registry.make_request("coupons".to_string(), "POST".to_string(), form_data) + .map_err(|e| e.to_string())?; + + coupon.id = Some(result.clone()); + Ok(result) + } + + // --- Getters --- + // Product getters + #[rhai_fn(get = "id", pure)] + pub fn get_product_id(product: &mut RhaiProduct) -> String { + product.id.clone().unwrap_or_default() + } + + #[rhai_fn(get = "name", pure)] + pub fn get_product_name(product: &mut RhaiProduct) -> String { + product.name.clone() + } + + #[rhai_fn(get = "description", pure)] + pub fn get_product_description(product: &mut RhaiProduct) -> String { + product.description.clone().unwrap_or_default() + } + + // Price getters + #[rhai_fn(get = "id", pure)] + pub fn get_price_id(price: &mut RhaiPrice) -> String { + price.id.clone().unwrap_or_default() + } + + #[rhai_fn(get = "amount", pure)] + pub fn get_price_amount(price: &mut RhaiPrice) -> i64 { + price.unit_amount as i64 + } + + #[rhai_fn(get = "currency", pure)] + pub fn get_price_currency(price: &mut RhaiPrice) -> String { + price.currency.clone() + } + + // Subscription getters + #[rhai_fn(get = "id", pure)] + pub fn get_subscription_id(subscription: &mut RhaiSubscription) -> String { + subscription.id.clone().unwrap_or_default() + } + + #[rhai_fn(get = "customer", pure)] + pub fn get_subscription_customer(subscription: &mut RhaiSubscription) -> String { + subscription.customer.clone() + } + + // Payment Intent getters + #[rhai_fn(get = "id", pure)] + pub fn get_payment_intent_id(intent: &mut RhaiPaymentIntent) -> String { + intent.id.clone().unwrap_or_default() + } + + #[rhai_fn(get = "amount", pure)] + pub fn get_payment_intent_amount(intent: &mut RhaiPaymentIntent) -> i64 { + intent.amount as i64 + } + + #[rhai_fn(get = "currency", pure)] + pub fn get_payment_intent_currency(intent: &mut RhaiPaymentIntent) -> String { + intent.currency.clone() + } + + // Coupon getters + #[rhai_fn(get = "id", pure)] + pub fn get_coupon_id(coupon: &mut RhaiCoupon) -> String { + coupon.id.clone().unwrap_or_default() + } + + #[rhai_fn(get = "duration", pure)] + pub fn get_coupon_duration(coupon: &mut RhaiCoupon) -> String { + coupon.duration.clone() + } + + #[rhai_fn(get = "percent_off", pure)] + pub fn get_coupon_percent_off(coupon: &mut RhaiCoupon) -> i64 { + coupon.percent_off.unwrap_or(0) as i64 + } +} + +pub fn register_payment_rhai_module(engine: &mut Engine) { + let module = exported_module!(rhai_payment_module); + + // Register custom types + engine.register_type_with_name::("Product"); + engine.register_type_with_name::("Price"); + engine.register_type_with_name::("Subscription"); + engine.register_type_with_name::("PaymentIntent"); + engine.register_type_with_name::("Coupon"); + + engine.register_global_module(module.into()); + println!("Successfully registered payment Rhai module."); +} \ No newline at end of file diff --git a/src/worker/cmd/README.md b/src/worker/cmd/README.md new file mode 100644 index 0000000..eb33441 --- /dev/null +++ b/src/worker/cmd/README.md @@ -0,0 +1,113 @@ +# Rhai Worker Binary + +A command-line worker for executing Rhai scripts from Redis task queues. + +## Binary: `worker` + +### Installation + +Build the binary: +```bash +cargo build --bin worker --release +``` + +### Usage + +```bash +# Basic usage - requires circle public key +worker --circle-public-key + +# Custom Redis URL +worker -c --redis-url redis://localhost:6379/1 + +# Custom worker ID and database path +worker -c --worker-id my_worker --db-path /tmp/worker_db + +# Preserve tasks for debugging/benchmarking +worker -c --preserve-tasks + +# Remove timestamps from logs +worker -c --no-timestamp + +# Increase verbosity +worker -c -v # Debug logging +worker -c -vv # Full debug +worker -c -vvv # Trace logging +``` + +### Command-Line Options + +| Option | Short | Default | Description | +|--------|-------|---------|-------------| +| `--circle-public-key` | `-c` | **Required** | Circle public key to listen for tasks | +| `--redis-url` | `-r` | `redis://localhost:6379` | Redis connection URL | +| `--worker-id` | `-w` | `worker_1` | Unique worker identifier | +| `--preserve-tasks` | | `false` | Preserve task details after completion | +| `--db-path` | | `worker_rhai_temp_db` | Database path for Rhai engine | +| `--no-timestamp` | | `false` | Remove timestamps from log output | +| `--verbose` | `-v` | | Increase verbosity (stackable) | + +### Features + +- **Task Queue Processing**: Listens to Redis queues for Rhai script execution tasks +- **Performance Optimized**: Configured for maximum Rhai engine performance +- **Graceful Shutdown**: Supports shutdown signals for clean termination +- **Flexible Logging**: Configurable verbosity and timestamp control +- **Database Integration**: Uses heromodels for data persistence +- **Task Cleanup**: Optional task preservation for debugging/benchmarking + +### How It Works + +1. **Queue Listening**: Worker listens on Redis queue `rhailib:{circle_public_key}` +2. **Task Processing**: Receives task IDs, fetches task details from Redis +3. **Script Execution**: Executes Rhai scripts with configured engine +4. **Result Handling**: Updates task status and sends results to reply queues +5. **Cleanup**: Optionally cleans up task details after completion + +### Configuration Examples + +#### Development Worker +```bash +# Simple development worker +worker -c dev_circle_123 + +# Development with verbose logging (no timestamps) +worker -c dev_circle_123 -v --no-timestamp +``` + +#### Production Worker +```bash +# Production worker with custom configuration +worker \ + --circle-public-key prod_circle_456 \ + --redis-url redis://redis-server:6379/0 \ + --worker-id prod_worker_1 \ + --db-path /var/lib/worker/db \ + --preserve-tasks +``` + +#### Benchmarking Worker +```bash +# Worker optimized for benchmarking +worker \ + --circle-public-key bench_circle_789 \ + --preserve-tasks \ + --no-timestamp \ + -vv +``` + +### Error Handling + +The worker provides clear error messages for: +- Missing or invalid circle public key +- Redis connection failures +- Script execution errors +- Database access issues + +### Dependencies + +- `rhailib_engine`: Rhai engine with heromodels integration +- `redis`: Redis client for task queue management +- `rhai`: Script execution engine +- `clap`: Command-line argument parsing +- `env_logger`: Logging infrastructure \ No newline at end of file diff --git a/src/worker/cmd/worker.rs b/src/worker/cmd/worker.rs index 69e4946..c26c399 100644 --- a/src/worker/cmd/worker.rs +++ b/src/worker/cmd/worker.rs @@ -6,8 +6,8 @@ use tokio::sync::mpsc; #[derive(Parser, Debug)] #[command(author, version, about, long_about = None)] struct Args { - /// Public key of the circle to listen to - #[arg(short, long, default_value = "default_public_key")] + /// Public key of the circle to listen to (required) + #[arg(short, long, help = "Circle public key to listen for tasks")] circle_public_key: String, /// Redis URL @@ -25,14 +25,26 @@ struct Args { /// Root directory for engine database #[arg(long, default_value = "worker_rhai_temp_db")] db_path: String, + + /// Disable timestamps in log output + #[arg(long, help = "Remove timestamps from log output")] + no_timestamp: bool, } #[tokio::main] async fn main() -> Result<(), Box> { - env_logger::init(); - let args = Args::parse(); + // Configure env_logger with or without timestamps + if args.no_timestamp { + env_logger::Builder::from_default_env() + .format_timestamp(None) + .init(); + } else { + env_logger::init(); + } + + log::info!("Rhai Worker (binary) starting with performance-optimized engine."); log::info!( "Worker ID: {}, Circle Public Key: {}, Redis: {}",