implement payment dsl

This commit is contained in:
Timur Gordon 2025-07-08 22:55:47 +02:00
parent 7619e3b944
commit 525685cce4
16 changed files with 2136 additions and 28 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
target
worker_rhai_temp_db
dump.rdb
dump.rdb
.env

449
Cargo.lock generated
View File

@ -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"

View File

@ -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"

157
src/client/cmd/README.md Normal file
View File

@ -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 <CALLER_KEY> --circle-key <CIRCLE_KEY>
# Execute inline script
client -c <CALLER_KEY> -k <CIRCLE_KEY> --script "print('Hello World!')"
# Execute script from file
client -c <CALLER_KEY> -k <CIRCLE_KEY> --file script.rhai
# Use specific worker (defaults to circle key)
client -c <CALLER_KEY> -k <CIRCLE_KEY> -w <WORKER_KEY> --script "2 + 2"
# Custom Redis and timeout
client -c <CALLER_KEY> -k <CIRCLE_KEY> --redis-url redis://localhost:6379/1 --timeout 60
# Remove timestamps from logs
client -c <CALLER_KEY> -k <CIRCLE_KEY> --no-timestamp
# Increase verbosity
client -c <CALLER_KEY> -k <CIRCLE_KEY> -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

201
src/client/cmd/client.rs Normal file
View File

@ -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<String>,
/// 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<String>,
/// Path to Rhai script file
#[arg(short, long, help = "Path to Rhai script file")]
file: Option<String>,
/// 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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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(())
}

View File

@ -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,

View File

@ -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"

View File

@ -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

View File

@ -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.

View File

@ -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<EvalAltResult>> {
// 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(())
}

View File

@ -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}%`);

View File

@ -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<RhaiCircle, Box<EvalAltResult>> {
Ok(Circle::new())
}
#[rhai_fn(name = "new_circle", return_raw)]
pub fn new_circle() -> Result<RhaiCircle, Box<EvalAltResult>> {
Ok(Circle::new())

View File

@ -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.");
}

917
src/dsl/src/payment.rs Normal file
View File

@ -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<Option<AsyncFunctionRegistry>> = Mutex::new(None);
const STRIPE_API_BASE: &str = "https://api.stripe.com/v1";
#[derive(Debug, Clone)]
pub struct AsyncFunctionRegistry {
pub request_sender: Sender<AsyncRequest>,
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<String, String>,
pub response_sender: oneshot::Sender<Result<String, String>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ApiResponse {
pub id: Option<String>,
pub status: Option<String>,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct RhaiProduct {
pub id: Option<String>,
pub name: String,
pub description: Option<String>,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct RhaiPrice {
pub id: Option<String>,
pub unit_amount: u64,
pub currency: String,
pub recurring: Option<RecurringConfig>,
pub product: String,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct RecurringConfig {
pub interval: String, // "month", "year", "week", "day"
pub interval_count: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct RhaiSubscription {
pub id: Option<String>,
pub customer: String,
pub items: Vec<SubscriptionItem>,
pub metadata: HashMap<String, String>,
pub trial_period_days: Option<u32>,
pub coupon: Option<String>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct SubscriptionItem {
pub price: String,
pub quantity: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct RhaiPaymentIntent {
pub id: Option<String>,
pub amount: u64,
pub currency: String,
pub payment_method_types: Vec<String>,
pub customer: Option<String>,
pub description: Option<String>,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Default)]
pub struct RhaiCoupon {
pub id: Option<String>,
pub duration: String, // "once", "repeating", "forever"
pub percent_off: Option<u32>,
pub amount_off: Option<u64>,
pub currency: Option<String>,
pub duration_in_months: Option<u32>,
pub metadata: HashMap<String, String>,
}
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<AsyncRequest>) {
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<String, String> {
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<String, String>) -> Result<String, String> {
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<String, String> {
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<String, String> {
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<String, String> {
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<String, String> {
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<String, String> {
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<String, Box<EvalAltResult>> {
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<RhaiProduct, Box<EvalAltResult>> {
Ok(RhaiProduct::new())
}
#[rhai_fn(name = "name", return_raw)]
pub fn product_name(product: &mut RhaiProduct, name: String) -> Result<RhaiProduct, Box<EvalAltResult>> {
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<RhaiProduct, Box<EvalAltResult>> {
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<RhaiProduct, Box<EvalAltResult>> {
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<String, Box<EvalAltResult>> {
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<RhaiPrice, Box<EvalAltResult>> {
Ok(RhaiPrice::new())
}
#[rhai_fn(name = "amount", return_raw)]
pub fn price_amount(price: &mut RhaiPrice, amount: i64) -> Result<RhaiPrice, Box<EvalAltResult>> {
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<RhaiPrice, Box<EvalAltResult>> {
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<RhaiPrice, Box<EvalAltResult>> {
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<RhaiPrice, Box<EvalAltResult>> {
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<RhaiPrice, Box<EvalAltResult>> {
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<RhaiPrice, Box<EvalAltResult>> {
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<String, Box<EvalAltResult>> {
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<RhaiSubscription, Box<EvalAltResult>> {
Ok(RhaiSubscription::new())
}
#[rhai_fn(name = "customer", return_raw)]
pub fn subscription_customer(subscription: &mut RhaiSubscription, customer_id: String) -> Result<RhaiSubscription, Box<EvalAltResult>> {
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<RhaiSubscription, Box<EvalAltResult>> {
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<RhaiSubscription, Box<EvalAltResult>> {
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<RhaiSubscription, Box<EvalAltResult>> {
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<RhaiSubscription, Box<EvalAltResult>> {
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<RhaiSubscription, Box<EvalAltResult>> {
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<String, Box<EvalAltResult>> {
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<RhaiPaymentIntent, Box<EvalAltResult>> {
Ok(RhaiPaymentIntent::new())
}
#[rhai_fn(name = "amount", return_raw)]
pub fn payment_intent_amount(intent: &mut RhaiPaymentIntent, amount: i64) -> Result<RhaiPaymentIntent, Box<EvalAltResult>> {
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<RhaiPaymentIntent, Box<EvalAltResult>> {
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<RhaiPaymentIntent, Box<EvalAltResult>> {
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<RhaiPaymentIntent, Box<EvalAltResult>> {
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<RhaiPaymentIntent, Box<EvalAltResult>> {
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<RhaiPaymentIntent, Box<EvalAltResult>> {
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<String, Box<EvalAltResult>> {
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<RhaiCoupon, Box<EvalAltResult>> {
Ok(RhaiCoupon::new())
}
#[rhai_fn(name = "duration", return_raw)]
pub fn coupon_duration(coupon: &mut RhaiCoupon, duration: String) -> Result<RhaiCoupon, Box<EvalAltResult>> {
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<RhaiCoupon, Box<EvalAltResult>> {
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<RhaiCoupon, Box<EvalAltResult>> {
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<RhaiCoupon, Box<EvalAltResult>> {
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<RhaiCoupon, Box<EvalAltResult>> {
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<String, Box<EvalAltResult>> {
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::<RhaiProduct>("Product");
engine.register_type_with_name::<RhaiPrice>("Price");
engine.register_type_with_name::<RhaiSubscription>("Subscription");
engine.register_type_with_name::<RhaiPaymentIntent>("PaymentIntent");
engine.register_type_with_name::<RhaiCoupon>("Coupon");
engine.register_global_module(module.into());
println!("Successfully registered payment Rhai module.");
}

113
src/worker/cmd/README.md Normal file
View File

@ -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 <CIRCLE_PUBLIC_KEY>
# Custom Redis URL
worker -c <CIRCLE_PUBLIC_KEY> --redis-url redis://localhost:6379/1
# Custom worker ID and database path
worker -c <CIRCLE_PUBLIC_KEY> --worker-id my_worker --db-path /tmp/worker_db
# Preserve tasks for debugging/benchmarking
worker -c <CIRCLE_PUBLIC_KEY> --preserve-tasks
# Remove timestamps from logs
worker -c <CIRCLE_PUBLIC_KEY> --no-timestamp
# Increase verbosity
worker -c <CIRCLE_PUBLIC_KEY> -v # Debug logging
worker -c <CIRCLE_PUBLIC_KEY> -vv # Full debug
worker -c <CIRCLE_PUBLIC_KEY> -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

View File

@ -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<dyn std::error::Error + Send + Sync>> {
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: {}",