implement payment dsl
This commit is contained in:
		
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +1,4 @@ | ||||
| target | ||||
| worker_rhai_temp_db | ||||
| dump.rdb | ||||
| dump.rdb | ||||
| .env | ||||
							
								
								
									
										449
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										449
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -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" | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										157
									
								
								src/client/cmd/README.md
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										201
									
								
								src/client/cmd/client.rs
									
									
									
									
									
										Normal 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(()) | ||||
| } | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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" | ||||
|   | ||||
							
								
								
									
										5
									
								
								src/dsl/examples/payment/.env.example
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/dsl/examples/payment/.env.example
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										58
									
								
								src/dsl/examples/payment/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/dsl/examples/payment/README.md
									
									
									
									
									
										Normal 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. | ||||
							
								
								
									
										46
									
								
								src/dsl/examples/payment/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/dsl/examples/payment/main.rs
									
									
									
									
									
										Normal 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(()) | ||||
| } | ||||
							
								
								
									
										176
									
								
								src/dsl/examples/payment/payment.rhai
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								src/dsl/examples/payment/payment.rhai
									
									
									
									
									
										Normal 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}%`); | ||||
| @@ -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()) | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										917
									
								
								src/dsl/src/payment.rs
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										113
									
								
								src/worker/cmd/README.md
									
									
									
									
									
										Normal 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 | ||||
| @@ -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: {}", | ||||
|   | ||||
		Reference in New Issue
	
	Block a user