Merge branch 'main' of git.threefold.info:herocode/db
This commit is contained in:
16
.gitignore
vendored
16
.gitignore
vendored
@@ -4,3 +4,19 @@ target/
|
||||
*.wasm
|
||||
herovm_build/
|
||||
test_db
|
||||
|
||||
# Node.js
|
||||
**/node_modules/
|
||||
**/dist/
|
||||
**/*.log
|
||||
**/package-lock.json
|
||||
|
||||
# TypeScript
|
||||
**/*.js.map
|
||||
**/*.d.ts
|
||||
**/*.tsbuildinfo
|
||||
rhaiinterface/client/**/*.js
|
||||
rhaiinterface/server/**/*.js
|
||||
!rhaiinterface/server/examples/**/client.js
|
||||
!rhaiinterface/server/examples/webpack.config.js
|
||||
.vscode
|
||||
|
507
adapter_macros/Cargo.lock
generated
Normal file
507
adapter_macros/Cargo.lock
generated
Normal file
@@ -0,0 +1,507 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adapter_macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"rhai",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"const-random",
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
||||
dependencies = [
|
||||
"const-random-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random-macro"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"once_cell",
|
||||
"tiny-keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.172"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
|
||||
|
||||
[[package]]
|
||||
name = "rhai"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bitflags",
|
||||
"instant",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"rhai_codegen",
|
||||
"smallvec",
|
||||
"smartstring",
|
||||
"thin-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_codegen"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
|
||||
|
||||
[[package]]
|
||||
name = "smartstring"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"static_assertions",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thin-vec"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||
dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.14.2+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
|
||||
dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "46ec44dc15085cea82cf9c78f85a9114c463a369786585ad2882d1ff0b0acf40"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b895b5356fc36103d0f64dd1e94dfa7ac5633f1c9dd6e80fe9ec4adef69e09d"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a7ab927b2637c19b3dbe0965e75d8f2d30bdd697a1516191cad2ec4df8fb28a"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
8
adapter_macros/Cargo.toml
Normal file
8
adapter_macros/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[package]
|
||||
name = "adapter_macros"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
rhai = "1.21.0"
|
||||
chrono = "0.4"
|
77
adapter_macros/README.md
Normal file
77
adapter_macros/README.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# Rhai Adapter Macros (`adapter_macros`)
|
||||
|
||||
This crate provides utility macros to facilitate the integration of Rust code with the Rhai scripting engine, particularly for adapting function and method signatures.
|
||||
|
||||
## Purpose
|
||||
|
||||
Rhai often uses `i64` as its default integer type. When exposing Rust functions or methods that use other integer types (e.g., `u32`, `usize`), direct registration can lead to type mismatches or require manual conversion boilerplate in each registered function.
|
||||
|
||||
These macros help bridge this gap by wrapping your Rust functions/methods, automatically handling the conversion from Rhai's `i64` to the Rust-native integer type and providing more informative error messages if the conversion fails (e.g., due to overflow).
|
||||
|
||||
## Macros
|
||||
|
||||
### 1. `adapt_rhai_i64_input_fn!(rust_fn:path, rust_int_ty:ty)`
|
||||
|
||||
Adapts a standalone Rust function that takes a single argument of `rust_int_ty` and returns `Result<_, Box<rhai::EvalAltResult>>`.
|
||||
|
||||
- `rust_fn`: The path to your Rust function (e.g., `my_module::my_function`).
|
||||
- `rust_int_ty`: The integer type your Rust function expects (e.g., `u32`).
|
||||
|
||||
**Example Usage in Rust (when registering with Rhai Engine):**
|
||||
|
||||
```rust
|
||||
// In your Rust code where you set up the Rhai engine:
|
||||
// Assuming your_function(val: u32) -> Result<Something, Error> exists
|
||||
// and adapter_macros is a dependency.
|
||||
|
||||
engine.register_fn("my_rhai_func", adapter_macros::adapt_rhai_i64_input_fn!(my_module::your_function, u32));
|
||||
|
||||
// In Rhai script:
|
||||
// my_rhai_func(10); // 10 (i64) will be converted to u32 for your_function
|
||||
```
|
||||
|
||||
### 2. `adapt_rhai_i64_input_method!(struct_ty:ty, rust_method_name:ident, rust_int_ty:ty)`
|
||||
|
||||
Adapts a Rust instance method that takes `self` by value, a single integer argument of `rust_int_ty`, and returns `Self`. This is useful for builder-like patterns or methods that modify and return the instance.
|
||||
|
||||
- `struct_ty`: The type of the struct on which the method is defined (e.g., `MyStruct`).
|
||||
- `rust_method_name`: The identifier of the Rust method (e.g., `with_value`).
|
||||
- `rust_int_ty`: The integer type your Rust method's argument expects (e.g., `u16`).
|
||||
|
||||
**Example Usage in Rust (when registering with Rhai Engine):**
|
||||
|
||||
```rust
|
||||
// In your Rust code:
|
||||
// Assuming MyStruct has a method: fn with_value(self, val: u16) -> Self
|
||||
// and adapter_macros is a dependency.
|
||||
|
||||
engine.register_fn("with_value", adapter_macros::adapt_rhai_i64_input_method!(MyStruct, with_value, u16));
|
||||
|
||||
// In Rhai script:
|
||||
// let my_obj = MyStruct::new();
|
||||
// my_obj.with_value(5); // 5 (i64) will be converted to u16 for MyStruct::with_value
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
If the `i64` value from Rhai cannot be converted to `rust_int_ty` (e.g., an `i64` value of -1 is passed when `u32` is expected, or a value too large for `u16`), the macros will generate a `rhai::EvalAltResult::ErrorArithmetic` with a descriptive message and the script position.
|
||||
|
||||
## How It Works
|
||||
|
||||
The macros generate a closure that:
|
||||
1. Takes a `rhai::NativeCallContext` and an `i64` from the Rhai engine.
|
||||
2. Attempts to convert the `i64` to the specified `rust_int_ty` using `try_into()`.
|
||||
3. If conversion fails, it returns an `ErrorArithmetic` detailing the function/method name and the type conversion that failed.
|
||||
4. If conversion succeeds, it calls the original Rust function/method with the converted value.
|
||||
|
||||
## Adding to Your Project
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
adapter_macros = { path = "../adapter_macros" } # Or version = "0.1.0" if published
|
||||
rhai = "x.y.z" # Your rhai version
|
||||
```
|
||||
|
||||
Ensure the path to `adapter_macros` is correct if used locally.
|
184
adapter_macros/src/lib.rs
Normal file
184
adapter_macros/src/lib.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
/// Macro to adapt a Rust function taking a specific integer type (e.g., u32)
|
||||
/// to a Rhai function that expects an i64 argument.
|
||||
#[macro_export]
|
||||
macro_rules! adapt_rhai_i64_input_fn {
|
||||
($rust_fn:path, $rust_int_ty:ty) => {
|
||||
move |context: ::rhai::NativeCallContext, rhai_val: i64| -> Result<_, Box<::rhai::EvalAltResult>> {
|
||||
let rust_val: $rust_int_ty = rhai_val.try_into().map_err(|_e| {
|
||||
Box::new(::rhai::EvalAltResult::ErrorArithmetic(
|
||||
format!("Conversion error for arg in '{}' from i64 to {}", stringify!($rust_fn), stringify!($rust_int_ty)),
|
||||
context.position(),
|
||||
))
|
||||
})?;
|
||||
Ok($rust_fn(rust_val))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Macro to adapt a Rust instance method (taking self by value, one int arg, returns Self)
|
||||
/// to a Rhai function that expects an i64 for that integer argument.
|
||||
#[macro_export]
|
||||
macro_rules! adapt_rhai_i64_input_method {
|
||||
($struct_ty:ty, $rust_method_name:ident, $rust_int_ty:ty) => {
|
||||
move |context: ::rhai::NativeCallContext, instance: $struct_ty, rhai_val: i64| -> Result<$struct_ty, Box<::rhai::EvalAltResult>> {
|
||||
let rust_val: $rust_int_ty = rhai_val.try_into().map_err(|_e| {
|
||||
Box::new(::rhai::EvalAltResult::ErrorArithmetic(
|
||||
format!("Conversion error for arg in '{}::{}' from i64 to {}", stringify!($struct_ty), stringify!($rust_method_name), stringify!($rust_int_ty)),
|
||||
context.position(),
|
||||
))
|
||||
})?;
|
||||
Ok(instance.$rust_method_name(rust_val))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// --- Rhai Timestamp Helper Functions ---
|
||||
pub mod rhai_timestamp_helpers {
|
||||
use rhai::{INT, EvalAltResult, Position};
|
||||
use chrono::{DateTime, Utc, TimeZone};
|
||||
|
||||
pub fn datetime_to_rhai_timestamp(dt: &DateTime<Utc>) -> INT {
|
||||
dt.timestamp()
|
||||
}
|
||||
|
||||
pub fn option_datetime_to_rhai_timestamp(dt_opt: &Option<DateTime<Utc>>) -> Option<INT> {
|
||||
dt_opt.as_ref().map(datetime_to_rhai_timestamp)
|
||||
}
|
||||
|
||||
pub fn rhai_timestamp_to_datetime(ts: INT) -> Result<DateTime<Utc>, Box<EvalAltResult>> {
|
||||
Utc.timestamp_opt(ts, 0).single()
|
||||
.ok_or_else(|| Box::new(EvalAltResult::ErrorArithmetic(format!("Invalid Unix timestamp: {}", ts), Position::NONE)))
|
||||
}
|
||||
|
||||
pub fn option_rhai_timestamp_to_datetime(ts_opt: Option<INT>) -> Result<Option<DateTime<Utc>>, Box<EvalAltResult>> {
|
||||
match ts_opt {
|
||||
Some(ts) => rhai_timestamp_to_datetime(ts).map(Some),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Macro for Enum Accessors (String Conversion) ---
|
||||
#[macro_export]
|
||||
macro_rules! register_rhai_enum_accessors {
|
||||
($engine:expr, $struct_type:ty, $field_name:ident, $rhai_name:expr, $to_string_fn:path, $from_string_fn:path) => {
|
||||
$engine.register_get_set(
|
||||
$rhai_name,
|
||||
move |obj: &mut $struct_type| -> rhai::ImmutableString { $to_string_fn(&obj.$field_name) },
|
||||
move |obj: &mut $struct_type, val: rhai::ImmutableString| -> Result<(), Box<rhai::EvalAltResult>> {
|
||||
obj.$field_name = $from_string_fn(val.as_str())?;
|
||||
Ok(())
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// --- Macro for DateTime Accessors (Unix Timestamp INT) ---
|
||||
#[macro_export]
|
||||
macro_rules! register_rhai_datetime_accessors {
|
||||
($engine:expr, $struct_type:ty, $field_path:ident, $rhai_name:expr, _required) => {
|
||||
$engine.register_get_set(
|
||||
$rhai_name,
|
||||
move |obj: &mut $struct_type| -> rhai::INT {
|
||||
let field_value = &obj.$field_path;
|
||||
$crate::rhai_timestamp_helpers::datetime_to_rhai_timestamp(field_value)
|
||||
},
|
||||
move |obj: &mut $struct_type, val: rhai::INT| -> Result<(), Box<rhai::EvalAltResult>> {
|
||||
obj.$field_path = $crate::rhai_timestamp_helpers::rhai_timestamp_to_datetime(val)?;
|
||||
Ok(())
|
||||
}
|
||||
);
|
||||
};
|
||||
($engine:expr, $struct_type:ty, base_data.$field_name:ident, $rhai_name:expr, _required) => {
|
||||
$engine.register_get_set(
|
||||
$rhai_name,
|
||||
move |obj: &mut $struct_type| -> rhai::INT {
|
||||
let field_value = &obj.base_data.$field_name;
|
||||
$crate::rhai_timestamp_helpers::datetime_to_rhai_timestamp(field_value)
|
||||
},
|
||||
move |obj: &mut $struct_type, val: rhai::INT| -> Result<(), Box<rhai::EvalAltResult>> {
|
||||
obj.base_data.$field_name = $crate::rhai_timestamp_helpers::rhai_timestamp_to_datetime(val)?;
|
||||
Ok(())
|
||||
}
|
||||
);
|
||||
};
|
||||
($engine:expr, $struct_type:ty, $field_path:ident, $rhai_name:expr) => {
|
||||
$engine.register_get_set(
|
||||
$rhai_name,
|
||||
move |obj: &mut $struct_type| -> Option<rhai::INT> {
|
||||
let field_value = &obj.$field_path;
|
||||
$crate::rhai_timestamp_helpers::option_datetime_to_rhai_timestamp(field_value)
|
||||
},
|
||||
move |obj: &mut $struct_type, val_opt: Option<rhai::INT>| -> Result<(), Box<rhai::EvalAltResult>> {
|
||||
obj.$field_path = $crate::rhai_timestamp_helpers::option_rhai_timestamp_to_datetime(val_opt)?;
|
||||
Ok(())
|
||||
}
|
||||
);
|
||||
};
|
||||
($engine:expr, $struct_type:ty, base_data.$field_name:ident, $rhai_name:expr) => {
|
||||
$engine.register_get_set(
|
||||
$rhai_name,
|
||||
move |obj: &mut $struct_type| -> Option<rhai::INT> {
|
||||
let field_value = &obj.base_data.$field_name;
|
||||
$crate::rhai_timestamp_helpers::option_datetime_to_rhai_timestamp(field_value)
|
||||
},
|
||||
move |obj: &mut $struct_type, val_opt: Option<rhai::INT>| -> Result<(), Box<rhai::EvalAltResult>> {
|
||||
obj.base_data.$field_name = $crate::rhai_timestamp_helpers::option_rhai_timestamp_to_datetime(val_opt)?;
|
||||
Ok(())
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// --- Macro for Vec<String> Accessors ---
|
||||
#[macro_export]
|
||||
macro_rules! register_rhai_vec_string_accessors {
|
||||
($engine:expr, $struct_type:ty, $field_name:ident, $rhai_name:expr) => {
|
||||
$engine.register_get_set(
|
||||
$rhai_name,
|
||||
move |obj: &mut $struct_type| -> rhai::Array {
|
||||
obj.$field_name.iter().map(|s| rhai::Dynamic::from(rhai::ImmutableString::from(s.as_str()))).collect()
|
||||
},
|
||||
move |obj: &mut $struct_type, val: rhai::Array| {
|
||||
obj.$field_name = val.into_iter().map(|d| d.into_string().unwrap_or_default()).collect();
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// --- Macro for Generic Field Accessors (Example: ImmutableString) ---
|
||||
#[macro_export]
|
||||
macro_rules! register_rhai_field_accessors {
|
||||
($engine:expr, $struct_type:ty, $field_name:ident, $rhai_name:expr) => {
|
||||
$engine.register_get_set(
|
||||
$rhai_name,
|
||||
move |obj: &mut $struct_type| obj.$field_name.clone(), // Assuming cloneable and directly Rhai compatible
|
||||
move |obj: &mut $struct_type, val: rhai::Dynamic| { // Or specific type like ImmutableString
|
||||
// This part would need more specific handling based on expected type
|
||||
// For example, if it's always ImmutableString:
|
||||
// if let Ok(s) = val.into_immutable_string() { obj.$field_name = s.into_owned(); }
|
||||
// For now, let's assume it's a type that can be directly assigned from Dynamic if Dynamic holds the right type
|
||||
// This is a simplification; real use might need obj.$field_name = val.try_cast().unwrap_or_default();
|
||||
// However, register_get_set usually infers setter type from getter type.
|
||||
// If getter is T, setter is fn(&mut S, T)
|
||||
// So if getter is |obj| obj.field.clone() -> String, setter should be |obj, val: String|
|
||||
// Let's assume string for now if using ImmutableString for Rhai
|
||||
if let Ok(s_val) = val.into_immutable_string() {
|
||||
obj.$field_name = s_val.into(); // Assumes field_name is String
|
||||
} else {
|
||||
// Handle error or default
|
||||
eprintln!("Failed to cast for field {}", $rhai_name);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
($engine:expr, $struct_type:ty, $field_name:ident, $rhai_name:expr, $rhai_type:ty) => {
|
||||
$engine.register_get_set(
|
||||
$rhai_name,
|
||||
move |obj: &mut $struct_type| obj.$field_name.clone(),
|
||||
move |obj: &mut $struct_type, val: $rhai_type| {
|
||||
obj.$field_name = val;
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
8
heromodels/.gitignore
vendored
Normal file
8
heromodels/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
temp_calendar_db
|
||||
temp_flow_db
|
||||
temp_flow_rhai_db
|
||||
temp_project_db
|
||||
temp_governance_db
|
||||
temp_legal_db
|
||||
temp_legal_rhai_db
|
||||
data
|
367
heromodels/Cargo.lock
generated
367
heromodels/Cargo.lock
generated
@@ -2,6 +2,28 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adapter_macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"rhai",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"const-random",
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
@@ -17,6 +39,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.4.0"
|
||||
@@ -43,6 +71,12 @@ dependencies = [
|
||||
"virtue",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.17.0"
|
||||
@@ -51,9 +85,9 @@ checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.19"
|
||||
version = "1.2.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362"
|
||||
checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
@@ -66,9 +100,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.40"
|
||||
version = "0.4.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
|
||||
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
@@ -79,6 +113,26 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
||||
dependencies = [
|
||||
"const-random-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random-macro"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"once_cell",
|
||||
"tiny-keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
@@ -95,26 +149,64 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.15"
|
||||
name = "crunchy"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "heromodels"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"adapter_macros",
|
||||
"bincode",
|
||||
"chrono",
|
||||
"heromodels-derive",
|
||||
"heromodels_core",
|
||||
"ourdb",
|
||||
"rhai",
|
||||
"rhai_autobind_macros",
|
||||
"rhai_client_macros",
|
||||
"rhai_wrapper",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"tst",
|
||||
]
|
||||
|
||||
@@ -159,6 +251,21 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.77"
|
||||
@@ -181,6 +288,21 @@ version = "0.4.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "no-std-compat"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
|
||||
dependencies = [
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -195,6 +317,9 @@ name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ourdb"
|
||||
@@ -206,6 +331,12 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
@@ -233,6 +364,12 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
@@ -260,7 +397,86 @@ version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"getrandom 0.2.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bitflags",
|
||||
"instant",
|
||||
"no-std-compat",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"rhai_codegen",
|
||||
"rust_decimal",
|
||||
"smallvec",
|
||||
"smartstring",
|
||||
"thin-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_autobind_macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_client_macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rhai",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_codegen"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_macros_derive"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_wrapper"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"rhai",
|
||||
"rhai_macros_derive",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_decimal"
|
||||
version = "1.37.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faa7de2ba56ac291bd90c6b9bece784a52ae1411f9506544b3eae36dd2356d50"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -269,6 +485,12 @@ version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
@@ -289,6 +511,18 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
@@ -296,16 +530,70 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.100"
|
||||
name = "smallvec"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
|
||||
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
|
||||
|
||||
[[package]]
|
||||
name = "smartstring"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"static_assertions",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thin-vec"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
@@ -326,6 +614,15 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||
dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tst"
|
||||
version = "0.1.0"
|
||||
@@ -346,6 +643,12 @@ version = "0.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "virtue"
|
||||
version = "0.0.18"
|
||||
@@ -358,6 +661,15 @@ version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.14.2+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
|
||||
dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.100"
|
||||
@@ -418,9 +730,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.0"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
|
||||
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
@@ -459,36 +771,45 @@ checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.2"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252"
|
||||
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.4.0"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
|
||||
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.24"
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879"
|
||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.24"
|
||||
version = "0.8.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be"
|
||||
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@@ -7,9 +7,79 @@ authors = ["Your Name <your.email@example.com>"]
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
bincode = { version = "2", features = ["serde"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
ourdb = { path = "../ourdb" }
|
||||
tst = { path = "../tst" }
|
||||
heromodels-derive = { path = "../heromodels-derive" }
|
||||
heromodels_core = { path = "../heromodels_core" }
|
||||
rhai_autobind_macros = { path = "../../rhaj/rhai_autobind_macros" }
|
||||
rhai_wrapper = { path = "../../rhaj/rhai_wrapper" }
|
||||
rhai = { version = "1.21.0", features = ["std", "sync", "decimal", "internals"] } # Added "decimal" feature, sync for Arc<Mutex<>>
|
||||
adapter_macros = { path = "../adapter_macros" }
|
||||
rhai_client_macros = { path = "../rhai_client_macros" }
|
||||
strum = "0.26"
|
||||
strum_macros = "0.26"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
rhai = []
|
||||
|
||||
[dev-dependencies]
|
||||
chrono = "0.4"
|
||||
[[example]]
|
||||
name = "calendar_example"
|
||||
path = "examples/calendar_example/main.rs"
|
||||
|
||||
[[example]]
|
||||
name = "governance_proposal_example"
|
||||
path = "examples/governance_proposal_example/main.rs"
|
||||
|
||||
[[example]]
|
||||
name = "finance_example"
|
||||
path = "examples/finance_example/main.rs"
|
||||
|
||||
[[example]]
|
||||
name = "calendar_rhai"
|
||||
path = "examples/calendar_rhai/example.rs"
|
||||
|
||||
[[example]]
|
||||
name = "calendar_rhai_client"
|
||||
path = "examples/calendar_rhai_client/example.rs"
|
||||
|
||||
[[example]]
|
||||
name = "flow_rhai"
|
||||
path = "examples/flow_rhai/example.rs"
|
||||
|
||||
[[example]]
|
||||
name = "finance_rhai"
|
||||
path = "examples/finance_rhai/example.rs"
|
||||
|
||||
[[example]]
|
||||
name = "governance_rhai"
|
||||
path = "examples/governance_rhai/example.rs"
|
||||
|
||||
[[example]]
|
||||
name = "governance_rhai_client"
|
||||
path = "examples/governance_rhai_client/example.rs"
|
||||
|
||||
[[example]]
|
||||
name = "flow_example"
|
||||
path = "examples/flow_example.rs"
|
||||
|
||||
[[example]]
|
||||
name = "legal_rhai"
|
||||
path = "examples/legal_rhai/example.rs"
|
||||
required-features = ["rhai"]
|
||||
|
||||
|
||||
[[example]]
|
||||
name = "project_rhai"
|
||||
path = "examples/project_rhai/example.rs"
|
||||
required-features = ["rhai"]
|
||||
|
||||
[[example]]
|
||||
name = "biz_rhai"
|
||||
path = "examples/biz_rhai/example.rs"
|
||||
required-features = ["rhai"]
|
||||
|
BIN
heromodels/data/0.db
Normal file
BIN
heromodels/data/0.db
Normal file
Binary file not shown.
BIN
heromodels/data/lookup/data
Normal file
BIN
heromodels/data/lookup/data
Normal file
Binary file not shown.
@@ -3,6 +3,29 @@ use heromodels::models::userexample::user::user_index::{is_active, username};
|
||||
use heromodels::models::{Comment, User};
|
||||
use heromodels_core::Model;
|
||||
|
||||
// Helper function to print user details
|
||||
fn print_user_details(user: &User) {
|
||||
println!("\n--- User Details ---");
|
||||
println!("ID: {}", user.get_id());
|
||||
println!("Username: {}", user.username);
|
||||
println!("Email: {}", user.email);
|
||||
println!("Full Name: {}", user.full_name);
|
||||
println!("Active: {}", user.is_active);
|
||||
println!("Created At: {}", user.base_data.created_at);
|
||||
println!("Modified At: {}", user.base_data.modified_at);
|
||||
println!("Comments: {:?}", user.base_data.comments);
|
||||
}
|
||||
|
||||
// Helper function to print comment details
|
||||
fn print_comment_details(comment: &Comment) {
|
||||
println!("\n--- Comment Details ---");
|
||||
println!("ID: {}", comment.get_id());
|
||||
println!("User ID: {}", comment.user_id);
|
||||
println!("Content: {}", comment.content);
|
||||
println!("Created At: {}", comment.base_data.created_at);
|
||||
println!("Modified At: {}", comment.base_data.modified_at);
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Create a new DB instance in /tmp/ourdb, and reset before every run
|
||||
let db = heromodels::db::hero::OurDB::new("/tmp/ourdb", true).expect("Can create DB");
|
||||
@@ -10,50 +33,72 @@ fn main() {
|
||||
println!("Hero Models - Basic Usage Example");
|
||||
println!("================================");
|
||||
|
||||
// Create a new user using the fluent interface
|
||||
let user = User::new(1)
|
||||
// Create users with auto-generated IDs
|
||||
|
||||
// User 1
|
||||
let user1 = User::new()
|
||||
.username("johndoe")
|
||||
.email("john.doe@example.com")
|
||||
.full_name("John Doe")
|
||||
.is_active(false)
|
||||
.build();
|
||||
let user2 = User::new(2)
|
||||
|
||||
// User 2
|
||||
let user2 = User::new()
|
||||
.username("janesmith")
|
||||
.email("jane.smith@example.com")
|
||||
.full_name("Jane Smith")
|
||||
.is_active(true)
|
||||
.build();
|
||||
let user3 = User::new(3)
|
||||
|
||||
// User 3
|
||||
let user3 = User::new()
|
||||
.username("willism")
|
||||
.email("willis.masters@example.com")
|
||||
.full_name("Willis Masters")
|
||||
.is_active(true)
|
||||
.build();
|
||||
let user4 = User::new(4)
|
||||
|
||||
// User 4
|
||||
let user4 = User::new()
|
||||
.username("carrols")
|
||||
.email("carrol.smith@example.com")
|
||||
.full_name("Carrol Smith")
|
||||
.is_active(false)
|
||||
.build();
|
||||
|
||||
db.collection()
|
||||
.expect("can open user collection")
|
||||
.set(&user)
|
||||
.expect("can set user");
|
||||
db.collection()
|
||||
.expect("can open user collection")
|
||||
.set(&user2)
|
||||
.expect("can set user");
|
||||
db.collection()
|
||||
.expect("can open user collection")
|
||||
.set(&user3)
|
||||
.expect("can set user");
|
||||
db.collection()
|
||||
.expect("can open user collection")
|
||||
.set(&user4)
|
||||
.expect("can set user");
|
||||
// Save all users to database and get their assigned IDs and updated models
|
||||
let (user1_id, db_user1) = db.collection().expect("can open user collection").set(&user1).expect("can set user");
|
||||
let (user2_id, db_user2) = db.collection().expect("can open user collection").set(&user2).expect("can set user");
|
||||
let (user3_id, db_user3) = db.collection().expect("can open user collection").set(&user3).expect("can set user");
|
||||
let (user4_id, db_user4) = db.collection().expect("can open user collection").set(&user4).expect("can set user");
|
||||
|
||||
// Perform an indexed lookup on the Username
|
||||
println!("User 1 assigned ID: {}", user1_id);
|
||||
println!("User 2 assigned ID: {}", user2_id);
|
||||
println!("User 3 assigned ID: {}", user3_id);
|
||||
println!("User 4 assigned ID: {}", user4_id);
|
||||
|
||||
// We already have the updated models from the set method, so we don't need to retrieve them again
|
||||
|
||||
// Print all users retrieved from database
|
||||
println!("\n--- Users Retrieved from Database ---");
|
||||
println!("\n1. First user:");
|
||||
print_user_details(&db_user1);
|
||||
|
||||
println!("\n2. Second user:");
|
||||
print_user_details(&db_user2);
|
||||
|
||||
println!("\n3. Third user:");
|
||||
print_user_details(&db_user3);
|
||||
|
||||
println!("\n4. Fourth user:");
|
||||
print_user_details(&db_user4);
|
||||
|
||||
// Demonstrate different ways to retrieve users from the database
|
||||
|
||||
// 1. Retrieve by username index
|
||||
println!("\n--- Retrieving Users by Different Methods ---");
|
||||
println!("\n1. By Username Index:");
|
||||
let stored_users = db
|
||||
.collection::<User>()
|
||||
.expect("can open user collection")
|
||||
@@ -61,72 +106,95 @@ fn main() {
|
||||
.expect("can load stored user");
|
||||
|
||||
assert_eq!(stored_users.len(), 1);
|
||||
let stored_user = &stored_users[0];
|
||||
print_user_details(&stored_users[0]);
|
||||
|
||||
assert_eq!(user.username, stored_user.username);
|
||||
assert_eq!(user.email, stored_user.email);
|
||||
assert_eq!(user.is_active, stored_user.is_active);
|
||||
assert_eq!(user.full_name, stored_user.full_name);
|
||||
|
||||
// Load all active users using the IsActive field index
|
||||
// TODO: expand Index type so it defines the type of the key
|
||||
// 2. Retrieve by active status
|
||||
println!("\n2. By Active Status (Active = true):");
|
||||
let active_users = db
|
||||
.collection::<User>()
|
||||
.expect("can open user collection")
|
||||
.get::<is_active, _>(&true)
|
||||
.expect("can load stored users");
|
||||
// We should have 2 active users
|
||||
assert_eq!(active_users.len(), 2);
|
||||
|
||||
// Now remove a user
|
||||
assert_eq!(active_users.len(), 2);
|
||||
for (_i, active_user) in active_users.iter().enumerate() {
|
||||
print_user_details(active_user);
|
||||
}
|
||||
|
||||
// 3. Delete a user and show the updated results
|
||||
println!("\n3. After Deleting a User:");
|
||||
let user_to_delete_id = active_users[0].get_id();
|
||||
println!("Deleting user with ID: {}", user_to_delete_id);
|
||||
db.collection::<User>()
|
||||
.expect("can open user collection")
|
||||
.delete_by_id(active_users[0].get_id())
|
||||
.delete_by_id(user_to_delete_id)
|
||||
.expect("can delete existing user");
|
||||
|
||||
// Load the active users again, should be 1 left
|
||||
// Show remaining active users
|
||||
let active_users = db
|
||||
.collection::<User>()
|
||||
.expect("can open user collection")
|
||||
.get::<is_active, _>(&true)
|
||||
.expect("can load stored users");
|
||||
|
||||
println!(" a. Remaining Active Users:");
|
||||
assert_eq!(active_users.len(), 1);
|
||||
// And verify we still have 2 inactive users
|
||||
for (_i, active_user) in active_users.iter().enumerate() {
|
||||
print_user_details(active_user);
|
||||
}
|
||||
|
||||
// Show inactive users
|
||||
let inactive_users = db
|
||||
.collection::<User>()
|
||||
.expect("can open user collection")
|
||||
.get::<is_active, _>(&false)
|
||||
.expect("can load stored users");
|
||||
assert_eq!(inactive_users.len(), 2);
|
||||
|
||||
println!("Created user: {:?}", user);
|
||||
println!("User ID: {}", user.get_id());
|
||||
println!(" b. Inactive Users:");
|
||||
assert_eq!(inactive_users.len(), 2);
|
||||
for (_i, inactive_user) in inactive_users.iter().enumerate() {
|
||||
print_user_details(inactive_user);
|
||||
}
|
||||
|
||||
println!("\n--- User Model Information ---");
|
||||
println!("User DB Prefix: {}", User::db_prefix());
|
||||
|
||||
// Create a comment for the user
|
||||
let comment = Comment::new(5)
|
||||
.user_id(1) // commenter's user ID
|
||||
// Demonstrate comment creation and association with a user
|
||||
println!("\n--- Working with Comments ---");
|
||||
|
||||
// 1. Create and save a comment
|
||||
println!("\n1. Creating a Comment:");
|
||||
let comment = Comment::new()
|
||||
.user_id(db_user1.get_id()) // commenter's user ID
|
||||
.content("This is a comment on the user")
|
||||
.build();
|
||||
|
||||
db.collection()
|
||||
.expect("can open commen collection")
|
||||
// Save the comment and get its assigned ID and updated model
|
||||
let (comment_id, db_comment) = db.collection()
|
||||
.expect("can open comment collection")
|
||||
.set(&comment)
|
||||
.expect("can set comment");
|
||||
|
||||
let stored_comment = db
|
||||
.collection::<Comment>()
|
||||
.expect("can open comment collection")
|
||||
.get_by_id(5)
|
||||
.expect("can load stored comment");
|
||||
println!("Comment assigned ID: {}", comment_id);
|
||||
|
||||
assert!(stored_comment.is_some());
|
||||
let stored_comment = stored_comment.unwrap();
|
||||
println!(" a. Comment Retrieved from Database:");
|
||||
print_comment_details(&db_comment);
|
||||
|
||||
assert_eq!(comment.get_id(), stored_comment.get_id());
|
||||
assert_eq!(comment.content, stored_comment.content);
|
||||
// 3. Associate the comment with a user
|
||||
println!("\n2. Associating Comment with User:");
|
||||
let mut updated_user = db_user1.clone();
|
||||
updated_user.base_data.add_comment(db_comment.get_id());
|
||||
|
||||
println!("\nCreated comment: {:?}", comment);
|
||||
println!("Comment ID: {}", comment.get_id());
|
||||
// Save the updated user and get the new version
|
||||
let (_, user_with_comment) = db.collection::<User>()
|
||||
.expect("can open user collection")
|
||||
.set(&updated_user)
|
||||
.expect("can set updated user");
|
||||
|
||||
println!(" a. User with Associated Comment:");
|
||||
print_user_details(&user_with_comment);
|
||||
|
||||
println!("\n--- Model Information ---");
|
||||
println!("User DB Prefix: {}", User::db_prefix());
|
||||
println!("Comment DB Prefix: {}", Comment::db_prefix());
|
||||
}
|
||||
|
275
heromodels/examples/biz_rhai/biz.rs
Normal file
275
heromodels/examples/biz_rhai/biz.rs
Normal file
@@ -0,0 +1,275 @@
|
||||
// Hero Models - Biz Rhai Example
|
||||
|
||||
print("Hero Models - Biz Rhai Example");
|
||||
print("===============================");
|
||||
print("DB instance will be implicitly passed to DB functions.");
|
||||
|
||||
// --- Enum Constants ---
|
||||
print("\n--- Enum Constants ---");
|
||||
print(`CompanyStatus Active: ${CompanyStatusConstants::Active}`);
|
||||
print(`CompanyStatus Inactive: ${CompanyStatusConstants::Inactive}`);
|
||||
print(`BusinessType Coop: ${BusinessTypeConstants::Coop}`);
|
||||
print(`BusinessType Global: ${BusinessTypeConstants::Global}`);
|
||||
|
||||
// --- Testing Company Model ---
|
||||
print("\n--- Testing Company Model ---");
|
||||
|
||||
let company1_uuid = "comp-uuid-alpha-001";
|
||||
let company1_name = "Innovatech Solutions Ltd.";
|
||||
let company1_reg = "REG-ISL-2024";
|
||||
let company1_inc_date = 1672531200; // Jan 1, 2023
|
||||
|
||||
print(`Creating a new company (UUID: ${company1_uuid})...`);
|
||||
let company1 = new_company(company1_name, company1_reg, company1_inc_date)
|
||||
.email("contact@innovatech.com")
|
||||
.phone("+1-555-0100")
|
||||
.website("https://innovatech.com")
|
||||
.address("123 Innovation Drive, Tech City, TC 54321")
|
||||
.business_type(BusinessTypeConstants::Global)
|
||||
.industry("Technology")
|
||||
.description("Leading provider of innovative tech solutions.")
|
||||
.status(CompanyStatusConstants::Active)
|
||||
.fiscal_year_end("12-31")
|
||||
.set_base_created_at(1672531200)
|
||||
.set_base_modified_at(1672531205);
|
||||
|
||||
print(`Company 1 Name: ${company1.name}, Status: ${company1.status}`);
|
||||
print(`Company 1 Email: ${company1.email}, Industry: ${company1.industry}`);
|
||||
// Save the company to the database
|
||||
print("\nSaving company1 to database...");
|
||||
company1 = set_company(company1); // Capture the company with the DB-assigned ID
|
||||
print("Company1 saved.");
|
||||
|
||||
// Retrieve the company
|
||||
print(`\nRetrieving company by ID (${company1.id})...`);
|
||||
let retrieved_company = get_company_by_id(company1.id);
|
||||
print(`Retrieved Company: ${retrieved_company.name}, Status: ${retrieved_company.status}`);
|
||||
print(`Retrieved Company Reg No: ${retrieved_company.registration_number}, Website: ${retrieved_company.website}`);
|
||||
|
||||
// --- Testing Shareholder Model ---
|
||||
print("\n--- Testing Shareholder Model ---");
|
||||
|
||||
|
||||
let sh1_user_id = 3001; // Example user ID
|
||||
let sh1_name = "Alice Wonderland";
|
||||
let sh1_since = 1672617600; // Example timestamp (Jan 2, 2023)
|
||||
|
||||
print(`Creating shareholder 1 for company ${company1.id}...`);
|
||||
let shareholder1 = new_shareholder()
|
||||
.company_id(company1.id)
|
||||
.user_id(sh1_user_id)
|
||||
.name(sh1_name)
|
||||
.shares(1000.0)
|
||||
.percentage(10.0) // CALCULATED
|
||||
.type_("Individual")
|
||||
.since(sh1_since)
|
||||
.set_base_created_at(1672617600);
|
||||
|
||||
shareholder1 = set_shareholder(shareholder1);
|
||||
print("Shareholder 1 saved.");
|
||||
|
||||
let retrieved_sh1 = get_shareholder_by_id(shareholder1.id);
|
||||
print(`Retrieved Shareholder 1: ${retrieved_sh1.name}, Type: ${retrieved_sh1.type_}, Shares: ${retrieved_sh1.shares}`);
|
||||
|
||||
|
||||
let sh2_entity_id = 4001; // Example corporate entity ID
|
||||
let sh2_name = "Mad Hatter Inc.";
|
||||
let sh2_since = 1672704000; // Example timestamp (Jan 3, 2023)
|
||||
|
||||
print(`\nCreating shareholder 2 for company ${company1.id}...`);
|
||||
let shareholder2 = new_shareholder()
|
||||
.company_id(company1.id)
|
||||
.user_id(sh2_entity_id) // Using user_id field for entity_id for simplicity in example
|
||||
.name(sh2_name)
|
||||
.shares(5000.0)
|
||||
.percentage(50.0)
|
||||
.type_(ShareholderTypeConstants::Corporate)
|
||||
.since(sh2_since)
|
||||
.set_base_created_at(1672704000);
|
||||
|
||||
shareholder2 = set_shareholder(shareholder2);
|
||||
print("Shareholder 2 saved.");
|
||||
|
||||
let retrieved_sh2 = get_shareholder_by_id(shareholder2.id);
|
||||
print(`Retrieved Shareholder 2: ${retrieved_sh2.name}, Type: ${retrieved_sh2.type_}, Percentage: ${retrieved_sh2.percentage}`);
|
||||
|
||||
// --- Testing Update for Company (Example - if setters were fully implemented for complex updates) ---
|
||||
print("\n--- Testing Update for Company ---");
|
||||
let updated_company = retrieved_company
|
||||
.description("Leading global provider of cutting-edge technology solutions and services.")
|
||||
.status(CompanyStatusConstants::Active)
|
||||
.phone("+1-555-0199"); // Assume modified_at would be updated by set_company
|
||||
|
||||
print(`Updated Company - Name: ${updated_company.name}, New Phone: ${updated_company.phone}`);
|
||||
set_company(updated_company);
|
||||
print("Updated Company saved.");
|
||||
|
||||
let final_retrieved_company = get_company_by_id(company1.id);
|
||||
print(`Final Retrieved Company - Description: '${final_retrieved_company.description}', Phone: ${final_retrieved_company.phone}`);
|
||||
|
||||
print("\n--- Testing Product Model ---");
|
||||
|
||||
// Print ProductType constants
|
||||
print("\n--- ProductType Constants ---");
|
||||
print(`Product Type Product: ${ProductTypeConstants::Product}`);
|
||||
print(`Product Type Service: ${ProductTypeConstants::Service}`);
|
||||
|
||||
// Print ProductStatus constants
|
||||
print("\n--- ProductStatus Constants ---");
|
||||
print(`Product Status Available: ${ProductStatusConstants::Available}`);
|
||||
print(`Product Status Unavailable: ${ProductStatusConstants::Unavailable}`);
|
||||
|
||||
// Create a product component
|
||||
let component1 = new_product_component("Super Capacitor")
|
||||
.description("High-capacity energy storage unit")
|
||||
.quantity(2);
|
||||
print(`\nCreated Product Component: ${component1.name}, Qty: ${component1.quantity}`);
|
||||
|
||||
// Create Product 1 (a physical product with a component)
|
||||
|
||||
let product1_name = "Advanced Gadget X";
|
||||
let product1_creation_time = 1672876800; // Example timestamp (Jan 5, 2023)
|
||||
|
||||
print(`\nCreating Product 1: ${product1_name}...`);
|
||||
let product1 = new_product()
|
||||
.name(product1_name)
|
||||
.description("A revolutionary gadget with cutting-edge features.")
|
||||
.price(299.99)
|
||||
.type_(ProductTypeConstants::Product)
|
||||
.category("Electronics")
|
||||
.status(ProductStatusConstants::Available)
|
||||
.max_amount(1000)
|
||||
.purchase_till(1704067199) // Dec 31, 2023, 23:59:59
|
||||
.active_till(1735689599) // Dec 31, 2024, 23:59:59
|
||||
.add_component(component1)
|
||||
.set_base_created_at(product1_creation_time);
|
||||
|
||||
print("Saving Product 1...");
|
||||
product1 = set_product(product1);
|
||||
print("Product 1 saved.");
|
||||
|
||||
print(`\nRetrieving Product 1 (ID: ${product1.id})...`);
|
||||
let retrieved_product1 = get_product_by_id(product1.id);
|
||||
print(`Retrieved Product 1: ${retrieved_product1.name}, Price: ${retrieved_product1.price}, Type: ${retrieved_product1.type_}`);
|
||||
if retrieved_product1.components.len() > 0 {
|
||||
print(`Product 1 Component 1: ${retrieved_product1.components[0].name}, Desc: ${retrieved_product1.components[0].description}`);
|
||||
} else {
|
||||
print("Product 1 has no components.");
|
||||
}
|
||||
|
||||
// Create Product 2 (a service)
|
||||
|
||||
let product2_name = "Cloud Backup Service - Pro Plan";
|
||||
let product2_creation_time = 1672963200; // Example timestamp (Jan 6, 2023)
|
||||
|
||||
print(`\nCreating Product 2: ${product2_name}...`);
|
||||
let product2 = new_product()
|
||||
.name(product2_name)
|
||||
.description("Unlimited cloud backup with 24/7 support.")
|
||||
.price(19.99) // Monthly price
|
||||
.type_(ProductTypeConstants::Service)
|
||||
.category("Cloud Services")
|
||||
.status(ProductStatusConstants::Available)
|
||||
.active_till(1735689599) // Valid for a long time, or represents subscription cycle end
|
||||
.set_base_created_at(product2_creation_time);
|
||||
|
||||
print("Saving Product 2...");
|
||||
product2 = set_product(product2);
|
||||
print("Product 2 saved.");
|
||||
|
||||
print(`\nRetrieving Product 2 (ID: ${product2.id})...`);
|
||||
let retrieved_product2 = get_product_by_id(product2.id);
|
||||
print(`Retrieved Product 2: ${retrieved_product2.name}, Category: ${retrieved_product2.category}, Status: ${retrieved_product2.status}`);
|
||||
if retrieved_product2.components.len() > 0 {
|
||||
print(`Product 2 has ${retrieved_product2.components.len()} components.`);
|
||||
} else {
|
||||
print("Product 2 has no components (as expected for a service).");
|
||||
}
|
||||
|
||||
|
||||
// --- Testing Sale Model ---
|
||||
print("\n--- Testing Sale Model ---");
|
||||
|
||||
// Print SaleStatus constants
|
||||
print("\n--- SaleStatus Constants ---");
|
||||
print(`Sale Status Pending: ${SaleStatusConstants::Pending}`);
|
||||
print(`Sale Status Completed: ${SaleStatusConstants::Completed}`);
|
||||
print(`Sale Status Cancelled: ${SaleStatusConstants::Cancelled}`);
|
||||
|
||||
// Create SaleItem 1 (using product1 from above)
|
||||
let sale_item1_product_id = product1.id; // Using product1.id after it's set // Using product1_id from product example
|
||||
let sale_item1_name = retrieved_product1.name; // Using name from retrieved product1
|
||||
let sale_item1_qty = 2;
|
||||
let sale_item1_unit_price = retrieved_product1.price;
|
||||
let sale_item1_subtotal = sale_item1_qty * sale_item1_unit_price;
|
||||
|
||||
print(`\nCreating SaleItem 1 for Product ID: ${sale_item1_product_id}, Name: ${sale_item1_name}...`);
|
||||
let sale_item1 = new_sale_item(sale_item1_product_id, sale_item1_name, sale_item1_qty, sale_item1_unit_price, sale_item1_subtotal);
|
||||
print(`SaleItem 1: Product ID ${sale_item1.product_id}, Qty: ${sale_item1.quantity}, Subtotal: ${sale_item1.subtotal}`);
|
||||
|
||||
// Create SaleItem 2 (using product2 from above)
|
||||
let sale_item2_product_id = product2.id; // Using product2.id after it's set // Using product2_id from product example
|
||||
let sale_item2_name = retrieved_product2.name;
|
||||
let sale_item2_qty = 1;
|
||||
let sale_item2_unit_price = retrieved_product2.price;
|
||||
let sale_item2_subtotal = sale_item2_qty * sale_item2_unit_price;
|
||||
|
||||
print(`\nCreating SaleItem 2 for Product ID: ${sale_item2_product_id}, Name: ${sale_item2_name}...`);
|
||||
let sale_item2 = new_sale_item(sale_item2_product_id, sale_item2_name, sale_item2_qty, sale_item2_unit_price, sale_item2_subtotal);
|
||||
print(`SaleItem 2: Product ID ${sale_item2.product_id}, Qty: ${sale_item2.quantity}, Subtotal: ${sale_item2.subtotal}`);
|
||||
|
||||
// Create a Sale
|
||||
|
||||
let sale1_customer_id = company1.id; // Example: company1 is the customer
|
||||
let sale1_date = 1673049600; // Example timestamp (Jan 7, 2023)
|
||||
let sale1_total_amount = sale_item1.subtotal + sale_item2.subtotal;
|
||||
|
||||
print(`\nCreating Sale 1 for Customer ID: ${sale1_customer_id}...`);
|
||||
let sale1 = new_sale(
|
||||
sale1_customer_id, // for company_id_i64 in Rhai registration
|
||||
"Temp Buyer Name", // for buyer_name in Rhai registration
|
||||
"temp@buyer.com", // for buyer_email in Rhai registration
|
||||
0.0, // for total_amount (will be overridden by builder)
|
||||
SaleStatusConstants::Pending, // for status (will be overridden by builder)
|
||||
0 // for sale_date (will be overridden by builder)
|
||||
)
|
||||
.customer_id(sale1_customer_id) // Actual field on Sale struct
|
||||
.status(SaleStatusConstants::Pending)
|
||||
.sale_date(sale1_date)
|
||||
.add_item(sale_item1) // Add item one by one
|
||||
.add_item(sale_item2)
|
||||
// Alternatively, to set all items at once (if items were already in an array):
|
||||
// .items([sale_item1, sale_item2])
|
||||
.total_amount(sale1_total_amount)
|
||||
.notes("First major sale of the year. Includes Advanced Gadget X and Cloud Backup Pro.")
|
||||
.set_base_created_at(sale1_date)
|
||||
.set_base_modified_at(sale1_date + 300) // 5 mins later
|
||||
.add_base_comment(1) // Example comment ID
|
||||
.add_base_comment(2);
|
||||
|
||||
print(`Sale 1 Created: ID ${sale1.id}, Customer ID: ${sale1.customer_id}, Status: ${sale1.status}, Total: ${sale1.total_amount}`);
|
||||
print(`Sale 1 Notes: ${sale1.notes}`);
|
||||
print(`Sale 1 Item Count: ${sale1.items.len()}`);
|
||||
if sale1.items.len() > 0 {
|
||||
print(`Sale 1 Item 1: ${sale1.items[0].name}, Qty: ${sale1.items[0].quantity}`);
|
||||
}
|
||||
if sale1.items.len() > 1 {
|
||||
print(`Sale 1 Item 2: ${sale1.items[1].name}, Qty: ${sale1.items[1].quantity}`);
|
||||
}
|
||||
print(`Sale 1 Base Comments: ${sale1.comments}`);
|
||||
|
||||
// Save Sale 1 to database
|
||||
print("\nSaving Sale 1 to database...");
|
||||
sale1 = set_sale(sale1);
|
||||
print("Sale 1 saved.");
|
||||
|
||||
// Retrieve Sale 1 from database
|
||||
print(`\nRetrieving Sale 1 by ID (${sale1.id})...`);
|
||||
let retrieved_sale1 = get_sale_by_id(sale1.id);
|
||||
print(`Retrieved Sale 1: ID ${retrieved_sale1.id}, Customer: ${retrieved_sale1.customer_id}, Status: ${retrieved_sale1.status}`);
|
||||
print(`Retrieved Sale 1 Total: ${retrieved_sale1.total_amount}, Notes: '${retrieved_sale1.notes}'`);
|
||||
if retrieved_sale1.items.len() > 0 {
|
||||
print(`Retrieved Sale 1 Item 1: ${retrieved_sale1.items[0].name} (Product ID: ${retrieved_sale1.items[0].product_id})`);
|
||||
}
|
||||
|
||||
print("\nBiz Rhai example script finished.");
|
41
heromodels/examples/biz_rhai/example.rs
Normal file
41
heromodels/examples/biz_rhai/example.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use rhai::{Engine, EvalAltResult, Scope};
|
||||
use std::sync::Arc;
|
||||
use heromodels::db::hero::OurDB; // Corrected path for OurDB
|
||||
use heromodels::models::biz::register_biz_rhai_module; // Corrected path
|
||||
use std::fs;
|
||||
|
||||
fn main() -> Result<(), Box<EvalAltResult>> {
|
||||
println!("Executing Rhai script: examples/biz_rhai/biz.rhai");
|
||||
|
||||
// Create a new Rhai engine
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Create an Arc<Mutex<OurDB>> instance
|
||||
// For this example, we'll use an in-memory DB.
|
||||
// The actual DB path or configuration might come from elsewhere in a real app.
|
||||
let db_instance = Arc::new(OurDB::new(".", false).expect("Failed to create DB")); // Corrected OurDB::new args and removed Mutex
|
||||
|
||||
// Register the biz module with the engine
|
||||
register_biz_rhai_module(&mut engine, Arc::clone(&db_instance));
|
||||
|
||||
// Read the Rhai script from file
|
||||
let script_path = "examples/biz_rhai/biz.rhai";
|
||||
let script_content = fs::read_to_string(script_path)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorSystem(format!("Cannot read script file: {}", script_path), e.into())))?;
|
||||
|
||||
// Create a new scope
|
||||
let mut scope = Scope::new();
|
||||
|
||||
// Execute the script
|
||||
match engine.run_with_scope(&mut scope, &script_content) {
|
||||
Ok(_) => {
|
||||
println!("Rhai script executed successfully!");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Rhai script execution failed: {}", e);
|
||||
println!("Details: {:?}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
132
heromodels/examples/calendar_example/main.rs
Normal file
132
heromodels/examples/calendar_example/main.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
use chrono::{Duration, Utc};
|
||||
use heromodels::db::{Collection, Db};
|
||||
use heromodels::models::calendar::{Attendee, AttendanceStatus, Calendar, Event};
|
||||
use heromodels_core::Model;
|
||||
|
||||
fn main() {
|
||||
// Create a new DB instance, reset before every run
|
||||
let db_path = "/tmp/ourdb_calendar_example";
|
||||
let db = heromodels::db::hero::OurDB::new(db_path, true).expect("Can create DB");
|
||||
|
||||
println!("Hero Models - Calendar Usage Example");
|
||||
println!("====================================");
|
||||
|
||||
// --- Create Attendees ---
|
||||
let attendee1 = Attendee::new("user_123".to_string())
|
||||
.status(AttendanceStatus::Accepted);
|
||||
let attendee2 = Attendee::new("user_456".to_string())
|
||||
.status(AttendanceStatus::Tentative);
|
||||
let attendee3 = Attendee::new("user_789".to_string()); // Default NoResponse
|
||||
|
||||
// --- Create Events ---
|
||||
let now = Utc::now();
|
||||
let event1 = Event::new(
|
||||
"event_alpha".to_string(),
|
||||
"Team Meeting",
|
||||
now + Duration::seconds(3600), // Using Duration::seconds for more explicit chrono 0.4 compatibility
|
||||
now + Duration::seconds(7200),
|
||||
)
|
||||
.description("Weekly sync-up meeting.")
|
||||
.location("Conference Room A")
|
||||
.add_attendee(attendee1.clone())
|
||||
.add_attendee(attendee2.clone());
|
||||
|
||||
let event2 = Event::new(
|
||||
"event_beta".to_string(),
|
||||
"Project Brainstorm",
|
||||
now + Duration::days(1),
|
||||
now + Duration::days(1) + Duration::seconds(5400), // 90 minutes
|
||||
)
|
||||
.description("Brainstorming session for new project features.")
|
||||
.add_attendee(attendee1.clone())
|
||||
.add_attendee(attendee3.clone());
|
||||
|
||||
let event3_for_calendar2 = Event::new(
|
||||
"event_gamma".to_string(),
|
||||
"Client Call",
|
||||
now + Duration::days(2),
|
||||
now + Duration::days(2) + Duration::seconds(3600)
|
||||
);
|
||||
|
||||
// --- Create Calendars ---
|
||||
// Note: Calendar::new directly returns Calendar, no separate .build() step like the user example.
|
||||
|
||||
// Create a calendar with auto-generated ID
|
||||
let calendar1 = Calendar::new(None, "Work Calendar")
|
||||
.description("Calendar for all work-related events.")
|
||||
.add_event(event1.clone())
|
||||
.add_event(event2.clone());
|
||||
|
||||
// Create a calendar with auto-generated ID (explicit IDs are no longer supported)
|
||||
let calendar2 = Calendar::new(None, "Personal Calendar")
|
||||
.add_event(event3_for_calendar2.clone());
|
||||
|
||||
|
||||
// --- Store Calendars in DB ---
|
||||
let cal_collection = db.collection::<Calendar>().expect("can open calendar collection");
|
||||
|
||||
let (_, calendar1) = cal_collection.set(&calendar1).expect("can set calendar1");
|
||||
let (_, calendar2) = cal_collection.set(&calendar2).expect("can set calendar2");
|
||||
|
||||
println!("Created calendar1 (ID: {}): Name - '{}'", calendar1.get_id(), calendar1.name);
|
||||
println!("Created calendar2 (ID: {}): Name - '{}'", calendar2.get_id(), calendar2.name);
|
||||
|
||||
// --- Retrieve a Calendar by ID ---
|
||||
let stored_calendar1_opt = cal_collection.get_by_id(calendar1.get_id()).expect("can try to load calendar1");
|
||||
assert!(stored_calendar1_opt.is_some(), "Calendar1 should be found in DB");
|
||||
let mut stored_calendar1 = stored_calendar1_opt.unwrap();
|
||||
|
||||
println!("\nRetrieved calendar1 from DB: Name - '{}', Events count: {}", stored_calendar1.name, stored_calendar1.events.len());
|
||||
assert_eq!(stored_calendar1.name, "Work Calendar");
|
||||
assert_eq!(stored_calendar1.events.len(), 2);
|
||||
assert_eq!(stored_calendar1.events[0].title, "Team Meeting");
|
||||
|
||||
// --- Modify a Calendar (Reschedule an Event) ---
|
||||
let event_id_to_reschedule = event1.id.as_str();
|
||||
let new_start_time = now + Duration::seconds(10800); // 3 hours from now
|
||||
let new_end_time = now + Duration::seconds(14400); // 4 hours from now
|
||||
|
||||
stored_calendar1 = stored_calendar1.update_event(event_id_to_reschedule, |event_to_update| {
|
||||
println!("Rescheduling event '{}'...", event_to_update.title);
|
||||
event_to_update.reschedule(new_start_time, new_end_time)
|
||||
});
|
||||
|
||||
let rescheduled_event = stored_calendar1.events.iter().find(|e| e.id == event_id_to_reschedule)
|
||||
.expect("Rescheduled event should exist");
|
||||
assert_eq!(rescheduled_event.start_time, new_start_time);
|
||||
assert_eq!(rescheduled_event.end_time, new_end_time);
|
||||
println!("Event '{}' rescheduled in stored_calendar1.", rescheduled_event.title);
|
||||
|
||||
// --- Store the modified calendar ---
|
||||
let (_, mut stored_calendar1) = cal_collection.set(&stored_calendar1).expect("can set modified calendar1");
|
||||
let re_retrieved_calendar1_opt = cal_collection.get_by_id(calendar1.get_id()).expect("can try to load modified calendar1");
|
||||
let re_retrieved_calendar1 = re_retrieved_calendar1_opt.unwrap();
|
||||
let re_retrieved_event = re_retrieved_calendar1.events.iter().find(|e| e.id == event_id_to_reschedule)
|
||||
.expect("Rescheduled event should exist in re-retrieved calendar");
|
||||
assert_eq!(re_retrieved_event.start_time, new_start_time, "Reschedule not persisted correctly");
|
||||
|
||||
println!("\nModified and re-saved calendar1. Rescheduled event start time: {}", re_retrieved_event.start_time);
|
||||
|
||||
// --- Add a new event to an existing calendar ---
|
||||
let event4_new = Event::new(
|
||||
"event_delta".to_string(),
|
||||
"1-on-1",
|
||||
now + Duration::days(3),
|
||||
now + Duration::days(3) + Duration::seconds(1800) // 30 minutes
|
||||
);
|
||||
stored_calendar1 = stored_calendar1.add_event(event4_new);
|
||||
assert_eq!(stored_calendar1.events.len(), 3);
|
||||
let (_, stored_calendar1) = cal_collection.set(&stored_calendar1).expect("can set calendar1 after adding new event");
|
||||
println!("Added new event '1-on-1' to stored_calendar1. Total events: {}", stored_calendar1.events.len());
|
||||
|
||||
// --- Delete a Calendar ---
|
||||
cal_collection.delete_by_id(calendar2.get_id()).expect("can delete calendar2");
|
||||
let deleted_calendar2_opt = cal_collection.get_by_id(calendar2.get_id()).expect("can try to load deleted calendar2");
|
||||
assert!(deleted_calendar2_opt.is_none(), "Calendar2 should be deleted from DB");
|
||||
|
||||
println!("\nDeleted calendar2 (ID: {}) from DB.", calendar2.get_id());
|
||||
println!("Calendar model DB Prefix: {}", Calendar::db_prefix());
|
||||
|
||||
println!("\nExample finished. DB stored at {}", db_path);
|
||||
println!("To clean up, you can manually delete the directory: {}", db_path);
|
||||
}
|
74
heromodels/examples/calendar_rhai/calendar.rhai
Normal file
74
heromodels/examples/calendar_rhai/calendar.rhai
Normal file
@@ -0,0 +1,74 @@
|
||||
// Get the database instance
|
||||
let db = get_db();
|
||||
|
||||
// Create a new calendar using the constructor and builder methods
|
||||
print("Creating a new calendar (ID will be DB-assigned) via registered constructor...");
|
||||
let calendar = new_calendar(). // ID removed
|
||||
name("My First Calendar").
|
||||
description("A calendar for testing Rhai integration");
|
||||
|
||||
let event = new_event(). // ID removed
|
||||
title("My First Event").
|
||||
description("An event for testing Rhai integration")
|
||||
.add_attendee(new_attendee(1)); // new_attendee(contact_id), not Attendee ID
|
||||
|
||||
// Add event's ID to calendar. event.id will be 0 if not saved separately.
|
||||
// Calendar::add_event returns the modified calendar, so we re-assign.
|
||||
calendar = calendar.add_event(event.id);
|
||||
|
||||
print("Type of calendar object: " + type_of(calendar));
|
||||
print("Created calendar: " + calendar.name);
|
||||
|
||||
// Save the calendar to the database and capture the result with DB-assigned ID
|
||||
let calendar = set_calendar(db, calendar); // Capture result
|
||||
print("Calendar saved to database");
|
||||
|
||||
// Check if calendar exists and retrieve it
|
||||
if calendar_exists(db, calendar.id) { // Use calendar.id from the saved object
|
||||
let retrieved_calendar = get_calendar_by_id(db, calendar.id); // Use calendar.id
|
||||
print("Retrieved calendar ID: " + retrieved_calendar.id + ", Name: " + retrieved_calendar.name);
|
||||
// Access the 'description' field directly.
|
||||
// Note: 'description' is Option<String>. Rhai handles options.
|
||||
// You might want to check for 'is_some()' or 'is_none()' or use 'unwrap_or()' pattern if needed.
|
||||
let desc = retrieved_calendar.description;
|
||||
if desc != () && desc != "" { // Check against '()' for None and empty string
|
||||
print("Description: " + desc);
|
||||
} else {
|
||||
print("No description available or it's None");
|
||||
}
|
||||
} else {
|
||||
print("Failed to retrieve calendar with ID " + calendar.id);
|
||||
}
|
||||
|
||||
// Create another calendar
|
||||
print("Creating another new calendar (ID will be DB-assigned) using builder methods...");
|
||||
let calendar2 = new_calendar(). // ID removed
|
||||
name("My Second Calendar").
|
||||
description("Another calendar for testing");
|
||||
|
||||
let calendar2 = set_calendar(db, calendar2); // Capture result
|
||||
print("Second calendar saved with ID: " + calendar2.id);
|
||||
|
||||
// Get all calendars
|
||||
let all_calendars = get_all_calendars(db);
|
||||
print("Total calendars: " + all_calendars.len());
|
||||
|
||||
for cal_item in all_calendars { // Renamed loop variable to avoid conflict if 'calendar' is still in scope
|
||||
// Access 'base_data.id' and 'name' fields directly
|
||||
print("Calendar ID: " + cal_item.base_data.id + ", Name: " + cal_item.name);
|
||||
}
|
||||
|
||||
// Delete a calendar
|
||||
delete_calendar_by_id(db, calendar.id); // Use ID from the first calendar object
|
||||
print("Attempted to delete calendar with ID " + calendar.id);
|
||||
|
||||
// Verify deletion
|
||||
if !calendar_exists(db, calendar.id) { // Use ID from the first calendar object
|
||||
print("Calendar with ID " + calendar.id + " was successfully deleted");
|
||||
} else {
|
||||
print("Failed to delete calendar with ID " + calendar.id + ", or it was not the one intended.");
|
||||
}
|
||||
|
||||
// Count remaining calendars
|
||||
let remaining_calendars = get_all_calendars(db);
|
||||
print("Remaining calendars: " + remaining_calendars.len());
|
83
heromodels/examples/calendar_rhai/example.rs
Normal file
83
heromodels/examples/calendar_rhai/example.rs
Normal file
@@ -0,0 +1,83 @@
|
||||
use heromodels::db::hero::OurDB;
|
||||
use heromodels::models::calendar::rhai::register_rhai_engine_functions;
|
||||
use rhai::Engine;
|
||||
use std::sync::Arc;
|
||||
use std::{fs, path::Path};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize Rhai engine
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Initialize database with OurDB
|
||||
let db = Arc::new(OurDB::new("temp_calendar_db", true).expect("Failed to create database"));
|
||||
|
||||
// Register the Calendar type with Rhai
|
||||
// This function is generated by the #[rhai_model_export] attribute
|
||||
Calendar::register_rhai_bindings_for_calendar(&mut engine, db.clone());
|
||||
|
||||
// Register a function to get the database instance
|
||||
engine.register_fn("get_db", move || db.clone());
|
||||
|
||||
// Register a calendar builder function
|
||||
engine.register_fn("calendar__builder", |id: i64| {
|
||||
let id_option = if id <= 0 { None } else { Some(id as u32) };
|
||||
Calendar::new(id_option, "New Calendar")
|
||||
});
|
||||
|
||||
// Register setter methods for Calendar properties
|
||||
engine.register_fn("set_description", |calendar: &mut Calendar, desc: String| {
|
||||
calendar.description = Some(desc);
|
||||
});
|
||||
|
||||
// Register getter methods for Calendar properties
|
||||
engine.register_fn("get_description", |calendar: Calendar| -> String {
|
||||
calendar.description.clone().unwrap_or_default()
|
||||
});
|
||||
|
||||
// Register getter for base_data.id
|
||||
engine.register_fn("get_id", |calendar: Calendar| -> i64 {
|
||||
calendar.base_data.id as i64
|
||||
});
|
||||
|
||||
// Register additional functions needed by the script
|
||||
engine.register_fn("set_calendar", |_db: Arc<OurDB>, _calendar: Calendar| {
|
||||
// In a real implementation, this would save the calendar to the database
|
||||
println!("Calendar saved: {}", _calendar.name);
|
||||
});
|
||||
|
||||
engine.register_fn("get_calendar_by_id", |_db: Arc<OurDB>, id: i64| -> Calendar {
|
||||
// In a real implementation, this would retrieve the calendar from the database
|
||||
Calendar::new(Some(id as u32), "Retrieved Calendar")
|
||||
});
|
||||
|
||||
// Register a function to check if a calendar exists
|
||||
engine.register_fn("calendar_exists", |_db: Arc<OurDB>, id: i64| -> bool {
|
||||
// In a real implementation, this would check if the calendar exists in the database
|
||||
id == 1 || id == 2
|
||||
});
|
||||
|
||||
// Define the function separately to use with the wrap_vec_return macro
|
||||
fn get_all_calendars(_db: Arc<OurDB>) -> Vec<Calendar> {
|
||||
// In a real implementation, this would retrieve all calendars from the database
|
||||
vec![Calendar::new(Some(1), "Calendar 1"), Calendar::new(Some(2), "Calendar 2")]
|
||||
}
|
||||
|
||||
// Register the function with the wrap_vec_return macro
|
||||
engine.register_fn("get_all_calendars", wrap_vec_return!(get_all_calendars, Arc<OurDB> => Calendar));
|
||||
|
||||
engine.register_fn("delete_calendar_by_id", |_db: Arc<OurDB>, _id: i64| {
|
||||
// In a real implementation, this would delete the calendar from the database
|
||||
println!("Calendar deleted with ID: {}", _id);
|
||||
});
|
||||
|
||||
// Load and evaluate the Rhai script
|
||||
let script_path = Path::new("examples/calendar_rhai/calendar.rhai");
|
||||
let script = fs::read_to_string(script_path)?;
|
||||
|
||||
match engine.eval::<()>(&script) {
|
||||
Ok(_) => println!("Script executed successfully!"),
|
||||
Err(e) => eprintln!("Script execution failed: {}", e),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@@ -1,3 +1,4 @@
|
||||
use heromodels::db::{Collection, Db};
|
||||
use heromodels_core::{BaseModelData, Model};
|
||||
use heromodels_derive::model;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -22,16 +23,35 @@ fn main() {
|
||||
println!("Hero Models - Custom Model Example");
|
||||
println!("==================================");
|
||||
|
||||
// Create a new DB instance, reset before every run
|
||||
let db_path = "/tmp/ourdb_custom_model_example";
|
||||
let db = heromodels::db::hero::OurDB::new(db_path, true).expect("Can create DB");
|
||||
|
||||
// Example usage of the generated implementation
|
||||
println!("CustomUser DB Prefix: {}", CustomUser::db_prefix());
|
||||
|
||||
let user = CustomUser {
|
||||
base_data: BaseModelData::new(1),
|
||||
base_data: BaseModelData::new(), // ID will be auto-generated by OurDB
|
||||
login: "johndoe".to_string(),
|
||||
is_active: true,
|
||||
full_name: "John Doe".to_string(),
|
||||
};
|
||||
|
||||
println!("\nCustomUser ID: {}", user.get_id());
|
||||
println!("CustomUser DB Keys: {:?}", user.db_keys());
|
||||
println!("\nBefore saving - CustomUser ID: {}", user.get_id());
|
||||
println!("Before saving - CustomUser DB Keys: {:?}", user.db_keys());
|
||||
|
||||
// Save the model to the database
|
||||
let collection = db.collection::<CustomUser>().expect("can open user collection");
|
||||
let (user_id, saved_user) = collection.set(&user).expect("can save user");
|
||||
|
||||
println!("\nAfter saving - CustomUser ID: {}", saved_user.get_id());
|
||||
println!("After saving - CustomUser DB Keys: {:?}", saved_user.db_keys());
|
||||
println!("Returned ID: {}", user_id);
|
||||
|
||||
// Verify that the ID was auto-generated
|
||||
assert_eq!(saved_user.get_id(), user_id);
|
||||
assert_ne!(saved_user.get_id(), 0);
|
||||
|
||||
println!("\nExample finished. DB stored at {}", db_path);
|
||||
println!("To clean up, you can manually delete the directory: {}", db_path);
|
||||
}
|
||||
|
331
heromodels/examples/finance_example/main.rs
Normal file
331
heromodels/examples/finance_example/main.rs
Normal file
@@ -0,0 +1,331 @@
|
||||
// heromodels/examples/finance_example/main.rs
|
||||
|
||||
use chrono::{Utc, Duration};
|
||||
use heromodels::models::finance::{Account, Asset, AssetType};
|
||||
use heromodels::models::finance::marketplace::{Listing, ListingType, ListingStatus, Bid, BidStatus};
|
||||
|
||||
fn main() {
|
||||
println!("Finance Models Example\n");
|
||||
|
||||
// --- PART 1: ACCOUNTS AND ASSETS ---
|
||||
println!("=== ACCOUNTS AND ASSETS ===\n");
|
||||
|
||||
// Create a new account with auto-generated ID
|
||||
let mut account = Account::new(
|
||||
None, // id (auto-generated)
|
||||
"Main ETH Wallet", // name
|
||||
1001, // user_id
|
||||
"My primary Ethereum wallet", // description
|
||||
"ethereum", // ledger
|
||||
"0x1234567890abcdef1234567890abcdef12345678", // address
|
||||
"0xpubkey123456789" // pubkey
|
||||
);
|
||||
|
||||
println!("Created Account: '{}' (ID: {})", account.name, account.base_data.id);
|
||||
println!("Owner: User {}", account.user_id);
|
||||
println!("Blockchain: {}", account.ledger);
|
||||
println!("Address: {}", account.address);
|
||||
println!("");
|
||||
|
||||
// Create some assets
|
||||
// Asset with auto-generated ID
|
||||
let eth_asset = Asset::new(
|
||||
None, // id (auto-generated)
|
||||
"Ethereum", // name
|
||||
"Native ETH cryptocurrency", // description
|
||||
1.5, // amount
|
||||
"0x0000000000000000000000000000000000000000", // address (ETH has no contract address)
|
||||
AssetType::Native, // asset_type
|
||||
18, // decimals
|
||||
);
|
||||
|
||||
// Assets with explicit IDs
|
||||
let usdc_asset = Asset::new(
|
||||
Some(102), // id
|
||||
"USDC", // name
|
||||
"USD Stablecoin on Ethereum", // description
|
||||
1000.0, // amount
|
||||
"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // address (USDC contract)
|
||||
AssetType::Erc20, // asset_type
|
||||
6, // decimals
|
||||
);
|
||||
|
||||
let nft_asset = Asset::new(
|
||||
Some(103), // id
|
||||
"CryptoPunk #1234", // name
|
||||
"Rare digital collectible", // description
|
||||
1.0, // amount
|
||||
"0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb", // address (CryptoPunks contract)
|
||||
AssetType::Erc721, // asset_type
|
||||
0, // decimals
|
||||
);
|
||||
|
||||
// Add assets to the account
|
||||
account = account.add_asset(eth_asset.clone());
|
||||
account = account.add_asset(usdc_asset.clone());
|
||||
account = account.add_asset(nft_asset.clone());
|
||||
|
||||
println!("Added Assets to Account:");
|
||||
for asset in &account.assets {
|
||||
println!("- {} ({:?}): {} units", asset.name, asset.asset_type, asset.formatted_amount());
|
||||
}
|
||||
|
||||
println!("\nTotal Account Value (raw sum): {}", account.total_value());
|
||||
println!("");
|
||||
|
||||
// Update account details
|
||||
account = account.update_details(
|
||||
Some("Primary Ethereum Wallet"), // new name
|
||||
None::<String>, // keep same description
|
||||
None::<String>, // keep same address
|
||||
Some("0xnewpubkey987654321"), // new pubkey
|
||||
);
|
||||
|
||||
println!("Updated Account Details:");
|
||||
println!("New Name: {}", account.name);
|
||||
println!("New Pubkey: {}", account.pubkey);
|
||||
println!("");
|
||||
|
||||
// Find an asset by name
|
||||
if let Some(found_asset) = account.find_asset_by_name("USDC") {
|
||||
println!("Found USDC Asset:");
|
||||
println!("- Amount: {} USDC", found_asset.formatted_amount());
|
||||
println!("- Contract: {}", found_asset.address);
|
||||
println!("");
|
||||
}
|
||||
|
||||
// --- PART 2: MARKETPLACE LISTINGS ---
|
||||
println!("\n=== MARKETPLACE LISTINGS ===\n");
|
||||
|
||||
// Create a fixed price listing with auto-generated ID
|
||||
let mut fixed_price_listing = Listing::new(
|
||||
None, // id (auto-generated)
|
||||
"1000 USDC for Sale", // title
|
||||
"Selling 1000 USDC tokens at fixed price", // description
|
||||
"102", // asset_id (referencing the USDC asset)
|
||||
AssetType::Erc20, // asset_type
|
||||
"1001", // seller_id
|
||||
1.05, // price (in ETH)
|
||||
"ETH", // currency
|
||||
ListingType::FixedPrice, // listing_type
|
||||
Some(Utc::now() + Duration::days(7)), // expires_at (7 days from now)
|
||||
vec!["token".to_string(), "stablecoin".to_string()], // tags
|
||||
Some("https://example.com/usdc.png"), // image_url
|
||||
);
|
||||
|
||||
println!("Created Fixed Price Listing: '{}' (ID: {})", fixed_price_listing.title, fixed_price_listing.base_data.id);
|
||||
println!("Price: {} {}", fixed_price_listing.price, fixed_price_listing.currency);
|
||||
println!("Type: {:?}, Status: {:?}", fixed_price_listing.listing_type, fixed_price_listing.status);
|
||||
println!("Expires: {}", fixed_price_listing.expires_at.unwrap());
|
||||
println!("");
|
||||
|
||||
// Complete the fixed price sale
|
||||
match fixed_price_listing.complete_sale("2001", 1.05) {
|
||||
Ok(updated_listing) => {
|
||||
fixed_price_listing = updated_listing;
|
||||
println!("Fixed Price Sale Completed:");
|
||||
println!("Status: {:?}", fixed_price_listing.status);
|
||||
println!("Buyer: {}", fixed_price_listing.buyer_id.unwrap());
|
||||
println!("Sale Price: {} {}", fixed_price_listing.sale_price.unwrap(), fixed_price_listing.currency);
|
||||
println!("Sold At: {}", fixed_price_listing.sold_at.unwrap());
|
||||
println!("");
|
||||
},
|
||||
Err(e) => println!("Error completing sale: {}", e),
|
||||
}
|
||||
|
||||
// Create an auction listing for the NFT with explicit ID
|
||||
let mut auction_listing = Listing::new(
|
||||
Some(202), // id (explicit)
|
||||
"CryptoPunk #1234 Auction", // title
|
||||
"Rare CryptoPunk NFT for auction", // description
|
||||
"103", // asset_id (referencing the NFT asset)
|
||||
AssetType::Erc721, // asset_type
|
||||
"1001", // seller_id
|
||||
10.0, // starting_price (in ETH)
|
||||
"ETH", // currency
|
||||
ListingType::Auction, // listing_type
|
||||
Some(Utc::now() + Duration::days(3)), // expires_at (3 days from now)
|
||||
vec!["nft".to_string(), "collectible".to_string(), "cryptopunk".to_string()], // tags
|
||||
Some("https://example.com/cryptopunk1234.png"), // image_url
|
||||
);
|
||||
|
||||
println!("Created Auction Listing: '{}' (ID: {})", auction_listing.title, auction_listing.base_data.id);
|
||||
println!("Starting Price: {} {}", auction_listing.price, auction_listing.currency);
|
||||
println!("Type: {:?}, Status: {:?}", auction_listing.listing_type, auction_listing.status);
|
||||
println!("");
|
||||
|
||||
// Create some bids
|
||||
let bid1 = Bid::new(
|
||||
auction_listing.base_data.id.to_string(), // listing_id
|
||||
2001, // bidder_id
|
||||
11.0, // amount
|
||||
"ETH", // currency
|
||||
);
|
||||
|
||||
let bid2 = Bid::new(
|
||||
auction_listing.base_data.id.to_string(), // listing_id
|
||||
2002, // bidder_id
|
||||
12.5, // amount
|
||||
"ETH", // currency
|
||||
);
|
||||
|
||||
let bid3 = Bid::new(
|
||||
auction_listing.base_data.id.to_string(), // listing_id
|
||||
2003, // bidder_id
|
||||
15.0, // amount
|
||||
"ETH", // currency
|
||||
);
|
||||
|
||||
// Add bids to the auction
|
||||
println!("Adding Bids to Auction:");
|
||||
|
||||
// Using clone() to avoid ownership issues with match expressions
|
||||
match auction_listing.clone().add_bid(bid1) {
|
||||
Ok(updated_listing) => {
|
||||
auction_listing = updated_listing;
|
||||
println!("- Bid added: 11.0 ETH from User 2001");
|
||||
},
|
||||
Err(e) => println!("Error adding bid: {}", e),
|
||||
}
|
||||
|
||||
match auction_listing.clone().add_bid(bid2) {
|
||||
Ok(updated_listing) => {
|
||||
auction_listing = updated_listing;
|
||||
println!("- Bid added: 12.5 ETH from User 2002");
|
||||
},
|
||||
Err(e) => println!("Error adding bid: {}", e),
|
||||
}
|
||||
|
||||
match auction_listing.clone().add_bid(bid3) {
|
||||
Ok(updated_listing) => {
|
||||
auction_listing = updated_listing;
|
||||
println!("- Bid added: 15.0 ETH from User 2003");
|
||||
},
|
||||
Err(e) => println!("Error adding bid: {}", e),
|
||||
}
|
||||
|
||||
println!("\nCurrent Auction Status:");
|
||||
println!("Current Price: {} {}", auction_listing.price, auction_listing.currency);
|
||||
|
||||
if let Some(highest_bid) = auction_listing.highest_bid() {
|
||||
println!("Highest Bid: {} {} from User {}",
|
||||
highest_bid.amount,
|
||||
highest_bid.currency,
|
||||
highest_bid.bidder_id);
|
||||
}
|
||||
|
||||
println!("Total Bids: {}", auction_listing.bids.len());
|
||||
println!("");
|
||||
|
||||
// Complete the auction
|
||||
match auction_listing.clone().complete_sale("2003", 15.0) {
|
||||
Ok(updated_listing) => {
|
||||
auction_listing = updated_listing;
|
||||
println!("Auction Completed:");
|
||||
println!("Status: {:?}", auction_listing.status);
|
||||
println!("Winner: User {}", auction_listing.buyer_id.as_ref().unwrap());
|
||||
println!("Winning Bid: {} {}", auction_listing.sale_price.as_ref().unwrap(), auction_listing.currency);
|
||||
println!("");
|
||||
|
||||
println!("Final Bid Statuses:");
|
||||
for bid in &auction_listing.bids {
|
||||
println!("- User {}: {} {} (Status: {:?})",
|
||||
bid.bidder_id,
|
||||
bid.amount,
|
||||
bid.currency,
|
||||
bid.status);
|
||||
}
|
||||
println!("");
|
||||
},
|
||||
Err(e) => println!("Error completing auction: {}", e),
|
||||
}
|
||||
|
||||
// Create an exchange listing with auto-generated ID
|
||||
let exchange_listing = Listing::new(
|
||||
None, // id (auto-generated)
|
||||
"ETH for BTC Exchange", // title
|
||||
"Looking to exchange ETH for BTC", // description
|
||||
"101", // asset_id (referencing the ETH asset)
|
||||
AssetType::Native, // asset_type
|
||||
"1001", // seller_id
|
||||
1.0, // amount (1 ETH)
|
||||
"BTC", // currency (what they want in exchange)
|
||||
ListingType::Exchange, // listing_type
|
||||
Some(Utc::now() + Duration::days(14)), // expires_at (14 days from now)
|
||||
vec!["exchange".to_string(), "crypto".to_string()], // tags
|
||||
None::<String>, // image_url
|
||||
);
|
||||
|
||||
println!("Created Exchange Listing: '{}' (ID: {})", exchange_listing.title, exchange_listing.base_data.id);
|
||||
println!("Offering: Asset {} ({:?})", exchange_listing.asset_id, exchange_listing.asset_type);
|
||||
println!("Wanted: {} {}", exchange_listing.price, exchange_listing.currency);
|
||||
println!("");
|
||||
|
||||
// --- PART 3: DEMONSTRATING EDGE CASES ---
|
||||
println!("\n=== EDGE CASES AND VALIDATIONS ===\n");
|
||||
|
||||
// Create a new auction listing for edge case testing with explicit ID
|
||||
let test_auction = Listing::new(
|
||||
Some(205), // id (explicit)
|
||||
"Test Auction", // title
|
||||
"For testing edge cases", // description
|
||||
"101", // asset_id
|
||||
AssetType::Native, // asset_type
|
||||
"1001", // seller_id
|
||||
10.0, // starting_price
|
||||
"ETH", // currency
|
||||
ListingType::Auction, // listing_type
|
||||
Some(Utc::now() + Duration::days(1)), // expires_at
|
||||
vec![], // tags
|
||||
None::<String>, // image_url
|
||||
);
|
||||
|
||||
// Try to add a bid that's too low
|
||||
let low_bid = Bid::new(
|
||||
test_auction.base_data.id.to_string(), // listing_id
|
||||
2004, // bidder_id
|
||||
5.0, // amount (lower than starting price)
|
||||
"ETH", // currency
|
||||
);
|
||||
|
||||
println!("Attempting to add a bid that's too low (5.0 ETH):");
|
||||
match test_auction.add_bid(low_bid) {
|
||||
Ok(_) => println!("Bid accepted (This shouldn't happen)"),
|
||||
Err(e) => println!("Error as expected: {}", e),
|
||||
}
|
||||
println!("");
|
||||
|
||||
// Try to cancel a completed listing
|
||||
println!("Attempting to cancel a completed listing:");
|
||||
match auction_listing.clone().cancel() {
|
||||
Ok(_) => println!("Listing cancelled (This shouldn't happen)"),
|
||||
Err(e) => println!("Error as expected: {}", e),
|
||||
}
|
||||
println!("");
|
||||
|
||||
// Create a listing that will expire with auto-generated ID
|
||||
let mut expiring_listing = Listing::new(
|
||||
None, // id (auto-generated)
|
||||
"About to Expire", // title
|
||||
"This listing will expire immediately", // description
|
||||
"101", // asset_id
|
||||
AssetType::Native, // asset_type
|
||||
"1001", // seller_id
|
||||
0.1, // price
|
||||
"ETH", // currency
|
||||
ListingType::FixedPrice, // listing_type
|
||||
Some(Utc::now() - Duration::hours(1)), // expires_at (1 hour ago)
|
||||
vec![], // tags
|
||||
None::<String>, // image_url
|
||||
);
|
||||
|
||||
println!("Created Expiring Listing: '{}' (ID: {})", expiring_listing.title, expiring_listing.base_data.id);
|
||||
println!("Initial Status: {:?}", expiring_listing.status);
|
||||
|
||||
// Check expiration
|
||||
expiring_listing = expiring_listing.check_expiration();
|
||||
println!("After Checking Expiration: {:?}", expiring_listing.status);
|
||||
println!("");
|
||||
|
||||
println!("Finance Models Example Completed.");
|
||||
}
|
109
heromodels/examples/finance_rhai/example.rs
Normal file
109
heromodels/examples/finance_rhai/example.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use rhai::{Engine, Scope, EvalAltResult};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
|
||||
// Import the models and the registration function
|
||||
use heromodels::models::finance::account::Account;
|
||||
use heromodels::models::finance::asset::{Asset};
|
||||
use heromodels::models::finance::marketplace::{Listing};
|
||||
use heromodels::models::finance::rhai::register_rhai_engine_functions;
|
||||
|
||||
// Define a simple in-memory mock database for the example
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MockDb {
|
||||
pub accounts: Arc<Mutex<HashMap<u32, Account>>>,
|
||||
pub assets: Arc<Mutex<HashMap<u32, Asset>>>,
|
||||
pub listings: Arc<Mutex<HashMap<u32, Listing>>>,
|
||||
// Bids are often part of Listings, so a separate HashMap for Bids might not be needed
|
||||
// unless we want to query bids globally by a unique bid ID.
|
||||
}
|
||||
|
||||
impl MockDb {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
accounts: Arc::new(Mutex::new(HashMap::new())),
|
||||
assets: Arc::new(Mutex::new(HashMap::new())),
|
||||
listings: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<EvalAltResult>> {
|
||||
println!("--- Finance Rhai Example ---");
|
||||
|
||||
let mut engine = Engine::new();
|
||||
let mut scope = Scope::new();
|
||||
|
||||
let mock_db = Arc::new(MockDb::new());
|
||||
|
||||
// Register finance functions and types with the engine
|
||||
register_rhai_engine_functions(
|
||||
&mut engine,
|
||||
Arc::clone(&mock_db.accounts),
|
||||
Arc::clone(&mock_db.assets),
|
||||
Arc::clone(&mock_db.listings)
|
||||
);
|
||||
println!("Rhai functions registered.");
|
||||
|
||||
scope.push("db_instance", mock_db.clone());
|
||||
|
||||
let script_path = "examples/finance_rhai/finance.rhai";
|
||||
println!("Loading script: {}", script_path);
|
||||
let script = match fs::read_to_string(script_path) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("Error reading script file '{}': {}", script_path, e);
|
||||
return Err(Box::new(EvalAltResult::ErrorSystem(
|
||||
"Failed to read script".to_string(),
|
||||
Box::new(e),
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
println!("Executing script...");
|
||||
match engine.run_with_scope(&mut scope, &script) {
|
||||
Ok(_) => println!("Script executed successfully!"),
|
||||
Err(e) => {
|
||||
eprintln!("Script execution failed: {:?}", e);
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
// Print final state of Accounts
|
||||
let final_accounts = mock_db.accounts.lock().unwrap();
|
||||
println!("\n--- Final Mock DB State (Accounts) ---");
|
||||
if final_accounts.is_empty() {
|
||||
println!("No accounts in mock DB.");
|
||||
}
|
||||
for (id, account) in final_accounts.iter() {
|
||||
println!("Account ID: {}, Name: '{}', User ID: {}, Assets: {}",
|
||||
id, account.name, account.user_id, account.assets.len());
|
||||
}
|
||||
|
||||
// Print final state of Assets
|
||||
let final_assets = mock_db.assets.lock().unwrap();
|
||||
println!("\n--- Final Mock DB State (Assets) ---");
|
||||
if final_assets.is_empty() {
|
||||
println!("No assets in mock DB.");
|
||||
}
|
||||
for (id, asset) in final_assets.iter() {
|
||||
println!("Asset ID: {}, Name: '{}', Amount: {}, Type: {:?}",
|
||||
id, asset.name, asset.amount, asset.asset_type);
|
||||
}
|
||||
|
||||
// Print final state of Listings
|
||||
let final_listings = mock_db.listings.lock().unwrap();
|
||||
println!("\n--- Final Mock DB State (Listings) ---");
|
||||
if final_listings.is_empty() {
|
||||
println!("No listings in mock DB.");
|
||||
}
|
||||
for (id, listing) in final_listings.iter() {
|
||||
println!(
|
||||
"Listing ID: {}, Title: '{}', Type: {:?}, Status: {:?}, Price: {}, Bids: {}",
|
||||
id, listing.title, listing.listing_type, listing.status, listing.price, listing.bids.len()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
143
heromodels/examples/finance_rhai/finance.rhai
Normal file
143
heromodels/examples/finance_rhai/finance.rhai
Normal file
@@ -0,0 +1,143 @@
|
||||
// Finance Rhai Script Example
|
||||
|
||||
print("--- Starting Finance Rhai Script ---");
|
||||
|
||||
// 1. Create an Account using the builder pattern
|
||||
let user1_id = 1; // Assuming this user_id is a separate concept, e.g. from an auth system
|
||||
let acc1 = new_account()
|
||||
.set_name("User1 Main Account")
|
||||
.set_user_id(user1_id) // user_id is i64 in Rhai, u32 in Rust. Conversion handled by setter.
|
||||
.set_description("Primary account for User 1")
|
||||
.set_ledger("LedgerX")
|
||||
.set_address("0x123MainSt")
|
||||
.set_pubkey("pubkeyUser1");
|
||||
print(`Created account (pre-save): ${acc1.name} with temp ID ${acc1.id}`);
|
||||
|
||||
// 2. Save Account to Mock DB and get the version with DB-assigned ID
|
||||
let acc1 = set_account(acc1); // Shadowing acc1 with the returned instance
|
||||
print(`Account ${acc1.name} saved to DB with ID ${acc1.id}.`);
|
||||
|
||||
// 3. Retrieve Account from Mock DB (using the new ID)
|
||||
let fetched_acc1 = get_account_by_id(acc1.id); // Use the ID from the saved acc1
|
||||
print(`Fetched account from DB: ${fetched_acc1.name}, User ID: ${fetched_acc1.user_id}`);
|
||||
|
||||
// 4. Create an Asset using the builder pattern
|
||||
let asset1 = new_asset()
|
||||
.set_name("HeroCoin")
|
||||
.set_description("Utility token for Hero Platform")
|
||||
.set_amount(1000.0)
|
||||
.set_address("0xTokenContract")
|
||||
.set_asset_type("Erc20") // Setter handles string to enum
|
||||
.set_decimals(18);
|
||||
print(`Created asset (pre-save): ${asset1.name} (temp ID: ${asset1.id}), Amount: ${asset1.amount}, Type: ${asset1.asset_type_str}`);
|
||||
|
||||
// 5. Save Asset to Mock DB and get the version with DB-assigned ID
|
||||
let asset1 = set_asset(asset1); // Shadowing asset1
|
||||
print(`Asset ${asset1.name} (ID: ${asset1.id}) saved to DB.`);
|
||||
|
||||
// 6. Retrieve Asset from Mock DB (using the new ID)
|
||||
let fetched_asset1 = get_asset_by_id(asset1.id); // Use the ID from the saved asset1
|
||||
print(`Fetched asset from DB: ${fetched_asset1.name}, Address: ${fetched_asset1.address}`);
|
||||
|
||||
// 7. Add Asset to Account
|
||||
// We have 'acc1' and 'asset1' from previous steps, both saved to DB and have their IDs.
|
||||
print(`Attempting to add asset ${asset1.id} to account ${acc1.id}`);
|
||||
|
||||
// Fetch the latest version of the account before modifying
|
||||
// let mut acc1_for_update = get_account_by_id(acc1.get_id());
|
||||
// // Fetch the asset to add (or use fetched_asset1 if it's the correct one)
|
||||
// let asset_to_add = get_asset_by_id(asset1.get_id());
|
||||
//
|
||||
// try {
|
||||
// acc1_for_update = acc1_for_update.add_asset(asset_to_add); // add_asset returns the modified account
|
||||
// acc1_for_update = set_account(acc1_for_update); // Save the account with the new asset
|
||||
// print(`Asset '${asset_to_add.name}' added to account '${acc1_for_update.name}'.`);
|
||||
// print(`Account now has ${acc1_for_update.get_assets_cloned().len()} assets.`);
|
||||
// // Verify the asset is there
|
||||
// if (acc1_for_update.get_assets_cloned().len() > 0) {
|
||||
// let first_asset_in_account = acc1_for_update.get_assets_cloned()[0];
|
||||
// print(`First asset in account: ${first_asset_in_account.name} (ID: ${first_asset_in_account.id})`);
|
||||
// }
|
||||
// } catch (err) {
|
||||
// print(`Error adding asset to account: ${err}`);
|
||||
// }
|
||||
|
||||
// 8. Create a Listing for the Asset using the builder pattern
|
||||
let current_timestamp = timestamp(); // Rhai's built-in for current unix timestamp (seconds)
|
||||
let expires_at_ts = current_timestamp + (24 * 60 * 60 * 7); // Expires in 7 days
|
||||
|
||||
let listing1 = new_listing()
|
||||
.set_title("Rare HeroCoin Batch")
|
||||
.set_description("100 HeroCoins for sale")
|
||||
.set_asset_id(asset1.id.to_string()) // Use ID from the saved asset1
|
||||
.set_asset_type_str("Erc20") // asset_type as string
|
||||
.set_seller_id(user1_id.to_string()) // seller_id as string (using the predefined user1_id)
|
||||
.set_price(50.0) // price
|
||||
.set_currency("USD") // currency
|
||||
.set_listing_type("FixedPrice") // listing_type as string
|
||||
.set_tags(["token", "herocoin", "sale"]); // tags as array of strings
|
||||
// image_url is None by default from new_listing(), so no need to call set_image_url_opt for None
|
||||
|
||||
print(`Created listing (pre-save): ${listing1.title} (temp ID: ${listing1.id}), Price: ${listing1.price} ${listing1.currency}`);
|
||||
print(`Listing type: ${listing1.listing_type}, Status: ${listing1.status}`);
|
||||
print(`Listing expires_at_ts_opt: ${listing1.expires_at_ts_opt}`);
|
||||
|
||||
// 9. Save Listing to Mock DB and get the version with DB-assigned ID
|
||||
let listing1 = set_listing(listing1); // Shadowing listing1
|
||||
print(`Listing ${listing1.get_title()} (ID: ${listing1.get_id()}) saved to DB.`);
|
||||
|
||||
// 10. Retrieve Listing from Mock DB (using the new ID)
|
||||
let fetched_listing1 = get_listing_by_id(listing1.get_id()); // Use the ID from the saved listing1
|
||||
print(`Fetched listing from DB: ${fetched_listing1.get_title()}, Seller ID: ${fetched_listing1.get_seller_id()}`);
|
||||
print(`Fetched listing asset_id: ${fetched_listing1.get_asset_id()}, asset_type: ${fetched_listing1.get_asset_type_str()}`);
|
||||
|
||||
// 11. Demonstrate an auction listing using the builder pattern
|
||||
let auction_listing = new_listing()
|
||||
.set_title("Vintage Hero Figurine")
|
||||
.set_description("Rare collectible, starting bid low!")
|
||||
.set_asset_id("asset_nft_123") // Mock asset ID for an NFT - this asset isn't created/saved in script
|
||||
.set_asset_type("Erc721")
|
||||
.set_seller_id(user1_id.to_string()) // Using the predefined user1_id
|
||||
.set_price(10.0) // Starting price
|
||||
.set_currency("USD")
|
||||
.set_listing_type("Auction")
|
||||
// expires_at_ts_opt is None by default
|
||||
.set_tags(["collectible", "rare", "auction"])
|
||||
.set_image_url_opt("http://example.com/figurine.png");
|
||||
|
||||
// Save Auction Listing to Mock DB and get the version with DB-assigned ID
|
||||
let auction_listing = set_listing(auction_listing); // Shadowing auction_listing
|
||||
print(`Created auction listing: ${auction_listing.get_title()} (ID: ${auction_listing.get_id()})`);
|
||||
|
||||
// 12. Create a Bid for the auction listing (Bid model not using builder pattern in this refactor)
|
||||
let bid1 = new_bid(auction_listing.get_id().to_string(), 2, 12.0, "USD"); // User 2 bids 12 USD
|
||||
print(`Created bid for listing ${bid1.listing_id} by bidder ${bid1.bidder_id} for ${bid1.amount} ${bid1.currency}`);
|
||||
print(`Bid status: ${bid1.status_str}, Created at: ${bid1.created_at_ts}`);
|
||||
|
||||
// 13. Add bid to listing
|
||||
let auction_listing_for_bid = get_listing_by_id(auction_listing.get_id());
|
||||
// print(`Listing '${auction_listing_for_bid.get_title()}' fetched for bidding. Current price: ${auction_listing_for_bid.get_price()}, Bids: ${auction_listing_for_bid.get_bids_cloned().len()}`);
|
||||
|
||||
try {
|
||||
let updated_listing_after_bid = auction_listing_for_bid.add_listing_bid(bid1);
|
||||
print(`Bid added to '${updated_listing_after_bid.get_title()}'. New bid count: ${updated_listing_after_bid.get_bids_cloned().len()}, New price: ${updated_listing_after_bid.get_price()};`);
|
||||
set_listing(updated_listing_after_bid); // Save updated listing to DB
|
||||
print("Auction listing with new bid saved to DB;");
|
||||
} catch (err) {
|
||||
print(`Error adding bid: ${err}`);
|
||||
}
|
||||
|
||||
// 14. Try to complete sale for the fixed price listing
|
||||
let listing_to_sell = get_listing_by_id(listing1.id);
|
||||
let buyer_user_id = 3;
|
||||
print(`Attempting to complete sale for listing: ${listing_to_sell.title} by buyer ${buyer_user_id}`);
|
||||
try {
|
||||
let sold_listing = listing_to_sell.complete_listing_sale(buyer_user_id.to_string(), listing_to_sell.price);
|
||||
print(`Sale completed for listing ${sold_listing.id}. New status: ${sold_listing.status}`);
|
||||
print(`Buyer ID: ${sold_listing.buyer_id_opt}, Sale Price: ${sold_listing.sale_price_opt}`);
|
||||
set_listing(sold_listing); // Save updated listing
|
||||
} catch (err) {
|
||||
print(`Error completing sale: ${err}`);
|
||||
}
|
||||
|
||||
print("--- Finance Rhai Script Finished ---");
|
163
heromodels/examples/flow_example.rs
Normal file
163
heromodels/examples/flow_example.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
use heromodels::db::{Collection, Db};
|
||||
use heromodels::models::flow::flow::flow_index::flow_uuid as flow_uuid_idx;
|
||||
use heromodels::models::flow::flow_step::flow_step_index::flow_id as flow_step_flow_id_idx;
|
||||
use heromodels::models::flow::{Flow, FlowStep, SignatureRequirement};
|
||||
use heromodels_core::Model;
|
||||
|
||||
// In a real application, you'd use the uuid crate for generating UUIDs:
|
||||
// use uuid::Uuid;
|
||||
|
||||
fn main() {
|
||||
// Create a new DB instance in /tmp/ourdb_flowbroker, and reset before every run
|
||||
let db = heromodels::db::hero::OurDB::new("/tmp/ourdb_flowbroker", true)
|
||||
.expect("Can create DB");
|
||||
|
||||
println!("Hero Models - Flow Example");
|
||||
println!("===========================");
|
||||
|
||||
// --- Create a Flow ---
|
||||
// In a real app: let new_flow_uuid = Uuid::new_v4().to_string();
|
||||
let new_flow_uuid = "a1b2c3d4-e5f6-7890-1234-567890abcdef"; // Example UUID
|
||||
|
||||
let flow1 = Flow::new(
|
||||
1, // id
|
||||
new_flow_uuid, // flow_uuid
|
||||
"Document Approval Flow", // name
|
||||
"Pending", // status
|
||||
);
|
||||
db.collection().expect("can open flow collection").set(&flow1).expect("can set flow1");
|
||||
println!("Created Flow: {:?}", flow1);
|
||||
println!("Flow ID: {}", flow1.get_id());
|
||||
println!("Flow DB Prefix: {}", Flow::db_prefix());
|
||||
|
||||
// --- Create FlowSteps for Flow1 ---
|
||||
let step1_flow1 = FlowStep::new(
|
||||
101, // id
|
||||
flow1.get_id(), // flow_id
|
||||
1, // step_order
|
||||
"Pending", // status
|
||||
)
|
||||
.description("Initial review by manager");
|
||||
db.collection().expect("can open flow_step collection").set(&step1_flow1).expect("can set step1_flow1");
|
||||
println!("Created FlowStep: {:?}", step1_flow1);
|
||||
|
||||
let step2_flow1 = FlowStep::new(
|
||||
102, // id
|
||||
flow1.get_id(), // flow_id
|
||||
2, // step_order
|
||||
"Pending", // status
|
||||
)
|
||||
.description("Legal team sign-off");
|
||||
db.collection().expect("can open flow_step collection").set(&step2_flow1).expect("can set step2_flow1");
|
||||
println!("Created FlowStep: {:?}", step2_flow1);
|
||||
|
||||
// --- Create SignatureRequirements for step2_flow1 ---
|
||||
let sig_req1_step2 = SignatureRequirement::new(
|
||||
201, // id
|
||||
step2_flow1.get_id(), // flow_step_id
|
||||
"pubkey_legal_team_lead_hex", // public_key
|
||||
"I approve this document for legal compliance.", // message
|
||||
"Pending", // status
|
||||
);
|
||||
db.collection().expect("can open sig_req collection").set(&sig_req1_step2).expect("can set sig_req1_step2");
|
||||
println!("Created SignatureRequirement: {:?}", sig_req1_step2);
|
||||
|
||||
let sig_req2_step2 = SignatureRequirement::new(
|
||||
202, // id
|
||||
step2_flow1.get_id(), // flow_step_id
|
||||
"pubkey_general_counsel_hex", // public_key
|
||||
"I, as General Counsel, approve this document.", // message
|
||||
"Pending", // status
|
||||
);
|
||||
db.collection().expect("can open sig_req collection").set(&sig_req2_step2).expect("can set sig_req2_step2");
|
||||
println!("Created SignatureRequirement: {:?}", sig_req2_step2);
|
||||
|
||||
// --- Retrieve and Verify ---
|
||||
// Get Flow by ID
|
||||
let retrieved_flow = db
|
||||
.collection::<Flow>()
|
||||
.expect("can open flow collection")
|
||||
.get_by_id(flow1.get_id())
|
||||
.expect("can load stored flow")
|
||||
.unwrap();
|
||||
assert_eq!(retrieved_flow.name, flow1.name);
|
||||
assert_eq!(retrieved_flow.flow_uuid, flow1.flow_uuid);
|
||||
println!("\nRetrieved Flow by ID: {:?}", retrieved_flow);
|
||||
|
||||
// Get Flow by flow_uuid (indexed lookup)
|
||||
let flows_by_uuid = db
|
||||
.collection::<Flow>()
|
||||
.expect("can open flow collection")
|
||||
.get::<flow_uuid_idx, _>(new_flow_uuid)
|
||||
.expect("can load flows by uuid");
|
||||
assert_eq!(flows_by_uuid.len(), 1);
|
||||
assert_eq!(flows_by_uuid[0].name, flow1.name);
|
||||
println!("Retrieved Flow by UUID (index): {:?}", flows_by_uuid[0]);
|
||||
|
||||
// Get FlowSteps for the retrieved_flow
|
||||
let steps_for_flow1 = db
|
||||
.collection::<FlowStep>()
|
||||
.expect("can open flow_step collection")
|
||||
.get::<flow_step_flow_id_idx, _>(&retrieved_flow.get_id())
|
||||
.expect("can load steps for flow1");
|
||||
assert_eq!(steps_for_flow1.len(), 2);
|
||||
println!("Retrieved {} FlowSteps for Flow ID {}:", steps_for_flow1.len(), retrieved_flow.get_id());
|
||||
for step in &steps_for_flow1 {
|
||||
println!(" - Step ID: {}, Order: {}, Desc: {:?}", step.get_id(), step.step_order, step.description);
|
||||
}
|
||||
|
||||
// --- Update a SignatureRequirement (simulate signing) ---
|
||||
let mut retrieved_sig_req1 = db
|
||||
.collection::<SignatureRequirement>()
|
||||
.expect("can open sig_req collection")
|
||||
.get_by_id(sig_req1_step2.get_id())
|
||||
.expect("can load sig_req1")
|
||||
.unwrap();
|
||||
|
||||
println!("\nUpdating SignatureRequirement ID: {}", retrieved_sig_req1.get_id());
|
||||
retrieved_sig_req1.status = "Signed".to_string();
|
||||
retrieved_sig_req1.signed_by = Some("pubkey_legal_team_lead_hex_actual_signer".to_string());
|
||||
retrieved_sig_req1.signature = Some("mock_signature_base64_encoded".to_string());
|
||||
|
||||
db.collection().expect("can open sig_req collection").set(&retrieved_sig_req1).expect("can update sig_req1");
|
||||
|
||||
let updated_sig_req1 = db
|
||||
.collection::<SignatureRequirement>()
|
||||
.expect("can open sig_req collection")
|
||||
.get_by_id(retrieved_sig_req1.get_id())
|
||||
.expect("can load updated sig_req1")
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(updated_sig_req1.status, "Signed");
|
||||
assert_eq!(updated_sig_req1.signature.as_deref(), Some("mock_signature_base64_encoded"));
|
||||
println!("Updated SignatureRequirement: {:?}", updated_sig_req1);
|
||||
|
||||
// --- Delete a FlowStep ---
|
||||
// (In a real app, you might also want to delete associated SignatureRequirements first, or handle via DB constraints/cascade if supported)
|
||||
let step1_id_to_delete = step1_flow1.get_id();
|
||||
db.collection::<FlowStep>()
|
||||
.expect("can open flow_step collection")
|
||||
.delete_by_id(step1_id_to_delete)
|
||||
.expect("can delete step1_flow1");
|
||||
println!("\nDeleted FlowStep ID: {}", step1_id_to_delete);
|
||||
|
||||
let deleted_step = db
|
||||
.collection::<FlowStep>()
|
||||
.expect("can open flow_step collection")
|
||||
.get_by_id(step1_id_to_delete)
|
||||
.expect("attempt to load deleted step");
|
||||
assert!(deleted_step.is_none());
|
||||
println!("Verified FlowStep ID {} is deleted.", step1_id_to_delete);
|
||||
|
||||
// Verify only one step remains for flow1
|
||||
let remaining_steps_for_flow1 = db
|
||||
.collection::<FlowStep>()
|
||||
.expect("can open flow_step collection")
|
||||
.get::<flow_step_flow_id_idx, _>(&retrieved_flow.get_id())
|
||||
.expect("can load remaining steps for flow1");
|
||||
assert_eq!(remaining_steps_for_flow1.len(), 1);
|
||||
assert_eq!(remaining_steps_for_flow1[0].get_id(), step2_flow1.get_id());
|
||||
println!("Remaining FlowSteps for Flow ID {}: count = {}", retrieved_flow.get_id(), remaining_steps_for_flow1.len());
|
||||
|
||||
println!("\nFlow example finished successfully!");
|
||||
}
|
36
heromodels/examples/flow_rhai/example.rs
Normal file
36
heromodels/examples/flow_rhai/example.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
use heromodels::db::hero::OurDB;
|
||||
use heromodels::models::flow::register_flow_rhai_module;
|
||||
use rhai::Engine;
|
||||
use std::sync::Arc;
|
||||
use std::{fs, path::Path};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize Rhai engine
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Initialize database with OurDB
|
||||
// Using a temporary/in-memory database for the example
|
||||
let db = Arc::new(OurDB::new("temp_flow_rhai_db", true).expect("Failed to create database"));
|
||||
|
||||
// Register flow Rhai module functions
|
||||
register_flow_rhai_module(&mut engine, db.clone());
|
||||
|
||||
// Load and evaluate the Rhai script
|
||||
let script_path_str = "examples/flow_rhai/flow.rhai";
|
||||
let script_path = Path::new(script_path_str);
|
||||
if !script_path.exists() {
|
||||
eprintln!("Error: Rhai script not found at {}", script_path_str);
|
||||
eprintln!("Please ensure the script 'flow.rhai' exists in the 'examples/flow_rhai/' directory.");
|
||||
return Err(Box::new(std::io::Error::new(std::io::ErrorKind::NotFound, format!("Rhai script not found: {}", script_path_str))));
|
||||
}
|
||||
|
||||
println!("Executing Rhai script: {}", script_path_str);
|
||||
let script = fs::read_to_string(script_path)?;
|
||||
|
||||
match engine.eval::<()>(&script) {
|
||||
Ok(_) => println!("\nRhai script executed successfully!"),
|
||||
Err(e) => eprintln!("\nRhai script execution failed: {}\nDetails: {:#?}", e, e),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
107
heromodels/examples/flow_rhai/flow.rs
Normal file
107
heromodels/examples/flow_rhai/flow.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
// Hero Models - Flow Rhai Example
|
||||
print("Hero Models - Flow Rhai Example");
|
||||
print("=============================");
|
||||
|
||||
// Helper to format Option<String> (Dynamic in Rhai: String or ()) for printing
|
||||
fn format_optional(val, placeholder) {
|
||||
if val == () {
|
||||
placeholder
|
||||
} else {
|
||||
val
|
||||
}
|
||||
}
|
||||
|
||||
// The database instance is now implicitly passed to DB functions.
|
||||
print("DB instance will be implicitly passed.");
|
||||
|
||||
// --- Test Flow Model ---
|
||||
print("\n--- Testing Flow Model ---");
|
||||
// Create a new flow using the constructor and builder methods
|
||||
print("Creating a new flow (ID: 1, UUID: flow-uuid-001)...");
|
||||
let flow1 = new_flow(1, "flow-uuid-001")
|
||||
.name("Document Approval Workflow")
|
||||
.status("Active");
|
||||
|
||||
print("Flow object created: " + flow1);
|
||||
print("Flow ID: " + flow1.id);
|
||||
print("Flow UUID: " + flow1.flow_uuid);
|
||||
print("Flow Name: " + flow1.name);
|
||||
print("Flow Status: " + flow1.status);
|
||||
|
||||
// Save the flow to the database
|
||||
set_flow(flow1);
|
||||
print("Flow saved to database.");
|
||||
|
||||
// Retrieve the flow
|
||||
let retrieved_flow = get_flow_by_id(1);
|
||||
print("Retrieved Flow by ID (1): " + retrieved_flow.name + ", Status: " + retrieved_flow.status);
|
||||
|
||||
// --- Test FlowStep Model (as part of Flow) ---
|
||||
print("\n--- Testing FlowStep Model (as part of Flow) ---");
|
||||
// Create FlowSteps
|
||||
print("Creating flow steps and adding to flow...");
|
||||
let step1 = new_flow_step(101, 1) // id, step_order
|
||||
.description("Initial Review by Manager")
|
||||
.status("Pending");
|
||||
|
||||
let step2 = new_flow_step(102, 2) // id, step_order. Note: FlowStep ID 102 will be used for sig_req1 & sig_req2
|
||||
.description("Legal Team Sign-off")
|
||||
.status("Pending");
|
||||
|
||||
// Add steps to the flow created earlier
|
||||
flow1 = flow1.add_step(step1);
|
||||
flow1 = flow1.add_step(step2);
|
||||
|
||||
print("Flow now has " + flow1.steps.len() + " steps.");
|
||||
print("First step description: " + format_optional(flow1.steps[0].description, "[No Description]"));
|
||||
|
||||
// Re-save the flow with its steps
|
||||
set_flow(flow1);
|
||||
print("Flow with steps saved to database.");
|
||||
|
||||
// Retrieve the flow and check its steps
|
||||
let retrieved_flow_with_steps = get_flow_by_id(1);
|
||||
print("Retrieved Flow by ID (1) has " + retrieved_flow_with_steps.steps.len() + " step(s).");
|
||||
if retrieved_flow_with_steps.steps.len() > 0 {
|
||||
print("First step of retrieved flow: " + format_optional(retrieved_flow_with_steps.steps[0].description, "[No Description]"));
|
||||
}
|
||||
|
||||
// --- Test SignatureRequirement Model ---
|
||||
print("\n--- Testing SignatureRequirement Model ---");
|
||||
// Create SignatureRequirements (referencing FlowStep ID 102, which is step2)
|
||||
print("Creating signature requirements for step with ID 102...");
|
||||
let sig_req1 = new_signature_requirement(201, 102, "pubkey_legal_lead", "Legal Lead: Approve terms.")
|
||||
.status("Required");
|
||||
|
||||
let sig_req2 = new_signature_requirement(202, 102, "pubkey_general_counsel", "General Counsel: Final Approval.")
|
||||
.status("Required"); // signed_by and signature will default to None (Rust) / () (Rhai)
|
||||
|
||||
print("SigReq 1: " + sig_req1.message + " for PubKey: " + sig_req1.public_key + " (Status: " + sig_req1.status + ")");
|
||||
if sig_req2.signed_by == () {
|
||||
print("SigReq 2: " + sig_req2.message + " for PubKey: " + sig_req2.public_key + " (Status: " + sig_req2.status + ", Not signed yet)");
|
||||
} else {
|
||||
print("SigReq 2: " + sig_req2.message + " for PubKey: " + sig_req2.public_key + " (Status: " + sig_req2.status + ", Signed by: " + format_optional(sig_req2.signed_by, "[Not Signed Yet]") + ")");
|
||||
}
|
||||
|
||||
|
||||
// Save signature requirements
|
||||
set_signature_requirement(sig_req1);
|
||||
set_signature_requirement(sig_req2);
|
||||
print("SignatureRequirements saved to database.");
|
||||
|
||||
// Retrieve a signature requirement
|
||||
let retrieved_sig_req = get_signature_requirement_by_id(201);
|
||||
print("Retrieved SignatureRequirement by ID (201): " + retrieved_sig_req.message);
|
||||
|
||||
// --- Test updating a SignatureRequirement ---
|
||||
print("\n--- Testing Update for SignatureRequirement ---");
|
||||
let updated_sig_req = retrieved_sig_req
|
||||
.status("Signed")
|
||||
.signed_by("pubkey_legal_lead_actual_signer_id")
|
||||
.signature("base64_encoded_signature_data_here");
|
||||
|
||||
print("Updated SigReq 1 - Status: " + updated_sig_req.status + ", Signed By: " + format_optional(updated_sig_req.signed_by, "[Not Signed Yet]") + ", Signature: " + format_optional(updated_sig_req.signature, "[No Signature]"));
|
||||
set_signature_requirement(updated_sig_req); // Save updated
|
||||
print("Updated SignatureRequirement saved.");
|
||||
|
||||
print("\nFlow Rhai example script finished.");
|
302
heromodels/examples/governance_proposal_example/main.rs
Normal file
302
heromodels/examples/governance_proposal_example/main.rs
Normal file
@@ -0,0 +1,302 @@
|
||||
// heromodels/examples/governance_proposal_example/main.rs
|
||||
|
||||
use chrono::{Duration, Utc};
|
||||
use heromodels::db::{Collection, Db};
|
||||
use heromodels::models::governance::{Proposal, ProposalStatus, VoteEventStatus};
|
||||
|
||||
fn main() {
|
||||
println!("Governance Proposal Model Example\n");
|
||||
|
||||
// Create a new DB instance, reset before every run
|
||||
let db_path = "/tmp/ourdb_governance_proposal_example";
|
||||
let db = heromodels::db::hero::OurDB::new(db_path, true).expect("Can create DB");
|
||||
|
||||
// Create a new proposal with auto-generated ID
|
||||
let mut proposal = Proposal::new(
|
||||
None, // id (auto-generated)
|
||||
"user_creator_123",
|
||||
"Ahmed fared", // creator_id
|
||||
"Community Fund Allocation for Q3", // title
|
||||
"Proposal to allocate funds for community projects in the third quarter.", // description
|
||||
ProposalStatus::Draft,
|
||||
Utc::now(), // created_at
|
||||
Utc::now(), // updated_at
|
||||
Utc::now(), // vote_start_date
|
||||
Utc::now() + Duration::days(14), // vote_end_date (14 days from now)
|
||||
);
|
||||
|
||||
println!(
|
||||
"Before saving - Created Proposal: '{}' (ID: {})",
|
||||
proposal.title, proposal.base_data.id
|
||||
);
|
||||
println!(
|
||||
"Before saving - Status: {:?}, Vote Status: {:?}",
|
||||
proposal.status, proposal.vote_status
|
||||
);
|
||||
println!(
|
||||
"Before saving - Vote Period: {} to {}\n",
|
||||
proposal.vote_start_date, proposal.vote_end_date
|
||||
);
|
||||
|
||||
// Add vote options
|
||||
proposal = proposal.add_option(1, "Approve Allocation", Some("This is the approval option"));
|
||||
proposal = proposal.add_option(2, "Reject Allocation", Some("This is the rejection option"));
|
||||
proposal = proposal.add_option(3, "Abstain", Some("This is the abstain option"));
|
||||
|
||||
println!("Added Vote Options:");
|
||||
for option in &proposal.options {
|
||||
println!(
|
||||
"- Option ID: {}, Text: '{}', Votes: {}",
|
||||
option.id, option.text, option.count
|
||||
);
|
||||
}
|
||||
println!("");
|
||||
|
||||
// Save the proposal to the database
|
||||
let collection = db
|
||||
.collection::<Proposal>()
|
||||
.expect("can open proposal collection");
|
||||
let (proposal_id, saved_proposal) = collection.set(&proposal).expect("can save proposal");
|
||||
|
||||
println!(
|
||||
"After saving - Proposal ID: {}",
|
||||
saved_proposal.base_data.id
|
||||
);
|
||||
println!("After saving - Returned ID: {}", proposal_id);
|
||||
|
||||
// Use the saved proposal for further operations
|
||||
proposal = saved_proposal;
|
||||
|
||||
// Simulate casting votes
|
||||
println!("Simulating Votes...");
|
||||
// User 1 votes for 'Approve Allocation' with 100 shares (with explicit ballot ID)
|
||||
proposal = proposal.cast_vote(Some(101), 1, 1, 100);
|
||||
// User 2 votes for 'Reject Allocation' with 50 shares (with explicit ballot ID)
|
||||
proposal = proposal.cast_vote(Some(102), 2, 2, 50);
|
||||
// User 3 votes for 'Approve Allocation' with 75 shares (with auto-generated ballot ID)
|
||||
proposal = proposal.cast_vote(None, 3, 1, 75);
|
||||
// User 4 abstains with 20 shares (with auto-generated ballot ID)
|
||||
proposal = proposal.cast_vote(None, 4, 3, 20);
|
||||
// User 5 attempts to vote for a non-existent option (should be handled gracefully)
|
||||
proposal = proposal.cast_vote(Some(105), 5, 99, 10);
|
||||
// User 1 tries to vote again (not explicitly prevented by current model, but could be a future enhancement)
|
||||
// proposal = proposal.cast_vote(Some(106), 1, 1, 10);
|
||||
|
||||
println!("\nVote Counts After Simulation:");
|
||||
for option in &proposal.options {
|
||||
println!(
|
||||
"- Option ID: {}, Text: '{}', Votes: {}",
|
||||
option.id, option.text, option.count
|
||||
);
|
||||
}
|
||||
|
||||
println!("\nBallots Cast:");
|
||||
for ballot in &proposal.ballots {
|
||||
println!(
|
||||
"- Ballot ID: {}, User ID: {}, Option ID: {}, Shares: {}",
|
||||
ballot.base_data.id, ballot.user_id, ballot.vote_option_id, ballot.shares_count
|
||||
);
|
||||
}
|
||||
println!("");
|
||||
|
||||
// Example of voting with comments using the cast_vote_with_comment method
|
||||
println!("Adding votes with comments...");
|
||||
|
||||
// User 7 votes for 'Approve Allocation' with a comment
|
||||
proposal = proposal.cast_vote_with_comment(
|
||||
Some(110), // ballot_id
|
||||
7, // user_id
|
||||
1, // chosen_option_id (Approve Allocation)
|
||||
80, // shares
|
||||
"I strongly support this proposal because it aligns with our community values.",
|
||||
);
|
||||
|
||||
// User 8 votes for 'Reject Allocation' with a comment
|
||||
proposal = proposal.cast_vote_with_comment(
|
||||
Some(111), // ballot_id
|
||||
8, // user_id
|
||||
2, // chosen_option_id (Reject Allocation)
|
||||
60, // shares
|
||||
"I have concerns about the allocation priorities.",
|
||||
);
|
||||
|
||||
println!("\nBallots with Comments:");
|
||||
for ballot in &proposal.ballots {
|
||||
if let Some(comment) = &ballot.comment {
|
||||
println!(
|
||||
"- Ballot ID: {}, User ID: {}, Option ID: {}, Shares: {}",
|
||||
ballot.base_data.id, ballot.user_id, ballot.vote_option_id, ballot.shares_count
|
||||
);
|
||||
println!(" Comment: \"{}\"", comment);
|
||||
}
|
||||
}
|
||||
|
||||
println!("\nUpdated Vote Counts After Comments:");
|
||||
for option in &proposal.options {
|
||||
println!(
|
||||
"- Option ID: {}, Text: '{}', Votes: {}",
|
||||
option.id, option.text, option.count
|
||||
);
|
||||
}
|
||||
|
||||
// Change proposal status
|
||||
proposal = proposal.change_proposal_status(ProposalStatus::Active);
|
||||
println!("Changed Proposal Status to: {:?}", proposal.status);
|
||||
|
||||
// Simulate closing the vote
|
||||
proposal = proposal.change_vote_event_status(VoteEventStatus::Closed);
|
||||
println!("Changed Vote Event Status to: {:?}", proposal.vote_status);
|
||||
|
||||
// Attempt to cast a vote after closing (should be handled)
|
||||
println!("\nAttempting to cast vote after voting is closed...");
|
||||
proposal = proposal.cast_vote(None, 6, 1, 25);
|
||||
|
||||
// Final proposal state
|
||||
println!("\nFinal Proposal State:");
|
||||
println!("Title: '{}'", proposal.title);
|
||||
println!("Status: {:?}", proposal.status);
|
||||
println!("Vote Status: {:?}", proposal.vote_status);
|
||||
println!("Options:");
|
||||
for option in &proposal.options {
|
||||
println!(
|
||||
" - {}: {} (Votes: {})",
|
||||
option.id, option.text, option.count
|
||||
);
|
||||
}
|
||||
println!("Total Ballots: {}", proposal.ballots.len());
|
||||
|
||||
// Example of a private proposal (not fully implemented in cast_vote eligibility yet)
|
||||
let mut private_proposal = Proposal::new(
|
||||
None, // auto-generated ID
|
||||
"user_admin_001",
|
||||
"Wael Ghonem",
|
||||
"Internal Team Restructure Vote",
|
||||
"Vote on proposed internal team changes.",
|
||||
ProposalStatus::Draft,
|
||||
Utc::now(),
|
||||
Utc::now(),
|
||||
Utc::now(),
|
||||
Utc::now() + Duration::days(7),
|
||||
);
|
||||
private_proposal.private_group = Some(vec![10, 20, 30]); // Only users 10, 20, 30 can vote
|
||||
private_proposal = private_proposal.add_option(
|
||||
1,
|
||||
"Accept Restructure",
|
||||
Some("This is the accept restructure option".to_string()),
|
||||
);
|
||||
private_proposal = private_proposal.add_option(
|
||||
2,
|
||||
"Reject Restructure",
|
||||
Some("This is the reject restructure option".to_string()),
|
||||
);
|
||||
|
||||
println!(
|
||||
"\nBefore saving - Created Private Proposal: '{}'",
|
||||
private_proposal.title
|
||||
);
|
||||
println!(
|
||||
"Before saving - Eligible Voters (Group): {:?}",
|
||||
private_proposal.private_group
|
||||
);
|
||||
|
||||
// Save the private proposal to the database
|
||||
let (private_proposal_id, saved_private_proposal) = collection
|
||||
.set(&private_proposal)
|
||||
.expect("can save private proposal");
|
||||
private_proposal = saved_private_proposal;
|
||||
|
||||
println!(
|
||||
"After saving - Private Proposal ID: {}",
|
||||
private_proposal.base_data.id
|
||||
);
|
||||
println!("After saving - Returned ID: {}", private_proposal_id);
|
||||
|
||||
// User 10 (eligible) votes with explicit ballot ID
|
||||
private_proposal = private_proposal.cast_vote(Some(201), 10, 1, 100);
|
||||
// User 40 (ineligible) tries to vote with auto-generated ballot ID
|
||||
private_proposal = private_proposal.cast_vote(None, 40, 1, 50);
|
||||
|
||||
// Example of voting with comments on a private proposal
|
||||
println!("\nAdding votes with comments to private proposal...");
|
||||
|
||||
// User 20 (eligible) votes with a comment
|
||||
private_proposal = private_proposal.cast_vote_with_comment(
|
||||
Some(202), // ballot_id
|
||||
20, // user_id (eligible)
|
||||
1, // chosen_option_id
|
||||
75, // shares
|
||||
"I support this restructuring plan with some reservations.",
|
||||
);
|
||||
|
||||
// User 30 (eligible) votes with a comment
|
||||
private_proposal = private_proposal.cast_vote_with_comment(
|
||||
Some(203), // ballot_id
|
||||
30, // user_id (eligible)
|
||||
2, // chosen_option_id
|
||||
90, // shares
|
||||
"I believe we should reconsider the timing of these changes.",
|
||||
);
|
||||
|
||||
// User 40 (ineligible) tries to vote with a comment
|
||||
private_proposal = private_proposal.cast_vote_with_comment(
|
||||
Some(204), // ballot_id
|
||||
40, // user_id (ineligible)
|
||||
1, // chosen_option_id
|
||||
50, // shares
|
||||
"This restructuring seems unnecessary.",
|
||||
);
|
||||
|
||||
println!("Eligible users 20 and 30 added votes with comments.");
|
||||
println!("Ineligible user 40 attempted to vote with a comment (should be rejected).");
|
||||
|
||||
println!("\nPrivate Proposal Ballots with Comments:");
|
||||
for ballot in &private_proposal.ballots {
|
||||
if let Some(comment) = &ballot.comment {
|
||||
println!(
|
||||
"- Ballot ID: {}, User ID: {}, Option ID: {}, Shares: {}",
|
||||
ballot.base_data.id, ballot.user_id, ballot.vote_option_id, ballot.shares_count
|
||||
);
|
||||
println!(" Comment: \"{}\"", comment);
|
||||
}
|
||||
}
|
||||
|
||||
println!("Private Proposal Vote Counts:");
|
||||
for option in &private_proposal.options {
|
||||
println!(
|
||||
" - {}: {} (Votes: {})",
|
||||
option.id, option.text, option.count
|
||||
);
|
||||
}
|
||||
|
||||
println!("\nExample finished. DB stored at {}", db_path);
|
||||
println!(
|
||||
"To clean up, you can manually delete the directory: {}",
|
||||
db_path
|
||||
);
|
||||
|
||||
// --- Additional Example: Listing and Filtering Proposals ---
|
||||
println!("\n--- Listing All Proposals ---");
|
||||
// List all proposals from the DB
|
||||
let all_proposals = collection.get_all().expect("can list all proposals");
|
||||
for proposal in &all_proposals {
|
||||
println!(
|
||||
"- Proposal ID: {}, Title: '{}', Status: {:?}",
|
||||
proposal.base_data.id, proposal.title, proposal.status
|
||||
);
|
||||
}
|
||||
println!("Total proposals in DB: {}", all_proposals.len());
|
||||
|
||||
// Filter proposals by status (e.g., only Active proposals)
|
||||
let active_proposals: Vec<_> = all_proposals
|
||||
.iter()
|
||||
.filter(|p| p.status == ProposalStatus::Active)
|
||||
.collect();
|
||||
println!("\n--- Filtering Proposals by Status: Active ---");
|
||||
for proposal in &active_proposals {
|
||||
println!(
|
||||
"- Proposal ID: {}, Title: '{}', Status: {:?}",
|
||||
proposal.base_data.id, proposal.title, proposal.status
|
||||
);
|
||||
}
|
||||
println!("Total ACTIVE proposals: {}", active_proposals.len());
|
||||
}
|
290
heromodels/examples/governance_rhai/example.rs
Normal file
290
heromodels/examples/governance_rhai/example.rs
Normal file
@@ -0,0 +1,290 @@
|
||||
use chrono::{Duration, Utc};
|
||||
use heromodels::db::hero::OurDB;
|
||||
use heromodels::models::governance::{
|
||||
Ballot, Proposal, ProposalStatus, VoteEventStatus, VoteOption,
|
||||
};
|
||||
use rhai::Engine;
|
||||
use rhai_wrapper::wrap_vec_return;
|
||||
use std::sync::Arc;
|
||||
use std::{fs, path::Path};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize Rhai engine
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Initialize database
|
||||
let db = Arc::new(OurDB::new("temp_governance_db", true).expect("Failed to create database"));
|
||||
|
||||
// Register the Proposal type with Rhai
|
||||
// This function is generated by the #[rhai_model_export] attribute
|
||||
Proposal::register_rhai_bindings_for_proposal(&mut engine, db.clone());
|
||||
|
||||
// Register the Ballot type with Rhai
|
||||
Ballot::register_rhai_bindings_for_ballot(&mut engine, db.clone());
|
||||
|
||||
// Register a function to get the database instance
|
||||
engine.register_fn("get_db", move || db.clone());
|
||||
|
||||
// Register builder functions for Proposal and related types
|
||||
engine.register_fn(
|
||||
"create_proposal",
|
||||
|id: i64, creator_id: String, creator_name: String, title: String, description: String| {
|
||||
let start_date = Utc::now();
|
||||
let end_date = start_date + Duration::days(14);
|
||||
let id_option = if id <= 0 { None } else { Some(id as u32) };
|
||||
Proposal::new(
|
||||
id_option,
|
||||
creator_id,
|
||||
creator_name,
|
||||
title,
|
||||
description,
|
||||
ProposalStatus::Draft,
|
||||
Utc::now(),
|
||||
Utc::now(),
|
||||
start_date,
|
||||
end_date,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
engine.register_fn("create_vote_option", |id: i64, text: String| {
|
||||
VoteOption::new(id as u8, text, Some("This is an optional comment"))
|
||||
});
|
||||
|
||||
engine.register_fn(
|
||||
"create_ballot",
|
||||
|id: i64, user_id: i64, vote_option_id: i64, shares_count: i64| {
|
||||
let id_option = if id <= 0 { None } else { Some(id as u32) };
|
||||
Ballot::new(
|
||||
id_option,
|
||||
user_id as u32,
|
||||
vote_option_id as u8,
|
||||
shares_count,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
// Register getter and setter methods for Proposal properties
|
||||
engine.register_fn("get_title", |proposal: Proposal| -> String {
|
||||
proposal.title.clone()
|
||||
});
|
||||
|
||||
engine.register_fn("get_description", |proposal: Proposal| -> String {
|
||||
proposal.description.clone()
|
||||
});
|
||||
|
||||
engine.register_fn("get_creator_id", |proposal: Proposal| -> String {
|
||||
proposal.creator_id.clone()
|
||||
});
|
||||
|
||||
engine.register_fn("get_id", |proposal: Proposal| -> i64 {
|
||||
proposal.base_data.id as i64
|
||||
});
|
||||
|
||||
engine.register_fn("get_status", |proposal: Proposal| -> String {
|
||||
format!("{:?}", proposal.status)
|
||||
});
|
||||
|
||||
engine.register_fn("get_vote_status", |proposal: Proposal| -> String {
|
||||
format!("{:?}", proposal.vote_status)
|
||||
});
|
||||
|
||||
// Register methods for proposal operations
|
||||
engine.register_fn(
|
||||
"add_option_to_proposal",
|
||||
|mut proposal: Proposal, option_id: i64, option_text: String| -> Proposal {
|
||||
proposal.add_option(
|
||||
option_id as u8,
|
||||
option_text,
|
||||
Some("This is an optional comment".to_string()),
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
engine.register_fn(
|
||||
"cast_vote_on_proposal",
|
||||
|mut proposal: Proposal,
|
||||
ballot_id: i64,
|
||||
user_id: i64,
|
||||
option_id: i64,
|
||||
shares: i64|
|
||||
-> Proposal {
|
||||
let ballot_id_option = if ballot_id <= 0 {
|
||||
None
|
||||
} else {
|
||||
Some(ballot_id as u32)
|
||||
};
|
||||
proposal.cast_vote(ballot_id_option, user_id as u32, option_id as u8, shares)
|
||||
},
|
||||
);
|
||||
|
||||
engine.register_fn(
|
||||
"change_proposal_status",
|
||||
|mut proposal: Proposal, status_str: String| -> Proposal {
|
||||
let new_status = match status_str.as_str() {
|
||||
"Draft" => ProposalStatus::Draft,
|
||||
"Active" => ProposalStatus::Active,
|
||||
"Approved" => ProposalStatus::Approved,
|
||||
"Rejected" => ProposalStatus::Rejected,
|
||||
"Cancelled" => ProposalStatus::Cancelled,
|
||||
_ => ProposalStatus::Draft,
|
||||
};
|
||||
proposal.change_proposal_status(new_status)
|
||||
},
|
||||
);
|
||||
|
||||
engine.register_fn(
|
||||
"change_vote_event_status",
|
||||
|mut proposal: Proposal, status_str: String| -> Proposal {
|
||||
let new_status = match status_str.as_str() {
|
||||
"Open" => VoteEventStatus::Open,
|
||||
"Closed" => VoteEventStatus::Closed,
|
||||
"Cancelled" => VoteEventStatus::Cancelled,
|
||||
_ => VoteEventStatus::Open,
|
||||
};
|
||||
proposal.change_vote_event_status(new_status)
|
||||
},
|
||||
);
|
||||
|
||||
// Register functions for database operations
|
||||
engine.register_fn("save_proposal", |_db: Arc<OurDB>, proposal: Proposal| {
|
||||
println!("Proposal saved: {}", proposal.title);
|
||||
});
|
||||
|
||||
engine.register_fn(
|
||||
"get_proposal_by_id",
|
||||
|_db: Arc<OurDB>, id: i64| -> Proposal {
|
||||
// In a real implementation, this would retrieve the proposal from the database
|
||||
let start_date = Utc::now();
|
||||
let end_date = start_date + Duration::days(14);
|
||||
Proposal::new(
|
||||
Some(id as u32),
|
||||
"Retrieved Creator",
|
||||
"Retrieved Creator Name",
|
||||
"Retrieved Proposal",
|
||||
"Retrieved Description",
|
||||
ProposalStatus::Draft,
|
||||
Utc::now(),
|
||||
Utc::now(),
|
||||
start_date,
|
||||
end_date,
|
||||
)
|
||||
},
|
||||
);
|
||||
|
||||
// Register a function to check if a proposal exists
|
||||
engine.register_fn("proposal_exists", |_db: Arc<OurDB>, id: i64| -> bool {
|
||||
// In a real implementation, this would check if the proposal exists in the database
|
||||
id == 1 || id == 2
|
||||
});
|
||||
|
||||
// Define the function for get_all_proposals
|
||||
fn get_all_proposals(_db: Arc<OurDB>) -> Vec<Proposal> {
|
||||
// In a real implementation, this would retrieve all proposals from the database
|
||||
let start_date = Utc::now();
|
||||
let end_date = start_date + Duration::days(14);
|
||||
vec![
|
||||
Proposal::new(
|
||||
Some(1),
|
||||
"Creator 1",
|
||||
"Creator Name 1",
|
||||
"Proposal 1",
|
||||
"Description 1",
|
||||
ProposalStatus::Draft,
|
||||
Utc::now(),
|
||||
Utc::now(),
|
||||
start_date,
|
||||
end_date,
|
||||
),
|
||||
Proposal::new(
|
||||
Some(2),
|
||||
"Creator 2",
|
||||
"Creator Name 2",
|
||||
"Proposal 2",
|
||||
"Description 2",
|
||||
ProposalStatus::Draft,
|
||||
Utc::now(),
|
||||
Utc::now(),
|
||||
start_date,
|
||||
end_date,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
// Register the function with the wrap_vec_return macro
|
||||
engine.register_fn(
|
||||
"get_all_proposals",
|
||||
wrap_vec_return!(get_all_proposals, Arc<OurDB> => Proposal),
|
||||
);
|
||||
|
||||
engine.register_fn("delete_proposal_by_id", |_db: Arc<OurDB>, _id: i64| {
|
||||
// In a real implementation, this would delete the proposal from the database
|
||||
println!("Proposal deleted with ID: {}", _id);
|
||||
});
|
||||
|
||||
// Register helper functions for accessing proposal options and ballots
|
||||
engine.register_fn("get_option_count", |proposal: Proposal| -> i64 {
|
||||
proposal.options.len() as i64
|
||||
});
|
||||
|
||||
engine.register_fn(
|
||||
"get_option_at",
|
||||
|proposal: Proposal, index: i64| -> VoteOption {
|
||||
if index >= 0 && index < proposal.options.len() as i64 {
|
||||
proposal.options[index as usize].clone()
|
||||
} else {
|
||||
VoteOption::new(
|
||||
0,
|
||||
"Invalid Option",
|
||||
Some("This is an invalid option".to_string()),
|
||||
)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
engine.register_fn("get_option_text", |option: VoteOption| -> String {
|
||||
option.text.clone()
|
||||
});
|
||||
|
||||
engine.register_fn("get_option_votes", |option: VoteOption| -> i64 {
|
||||
option.count
|
||||
});
|
||||
|
||||
engine.register_fn("get_ballot_count", |proposal: Proposal| -> i64 {
|
||||
proposal.ballots.len() as i64
|
||||
});
|
||||
|
||||
engine.register_fn(
|
||||
"get_ballot_at",
|
||||
|proposal: Proposal, index: i64| -> Ballot {
|
||||
if index >= 0 && index < proposal.ballots.len() as i64 {
|
||||
proposal.ballots[index as usize].clone()
|
||||
} else {
|
||||
Ballot::new(None, 0, 0, 0)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
engine.register_fn("get_ballot_user_id", |ballot: Ballot| -> i64 {
|
||||
ballot.user_id as i64
|
||||
});
|
||||
|
||||
engine.register_fn("get_ballot_option_id", |ballot: Ballot| -> i64 {
|
||||
ballot.vote_option_id as i64
|
||||
});
|
||||
|
||||
engine.register_fn("get_ballot_shares", |ballot: Ballot| -> i64 {
|
||||
ballot.shares_count
|
||||
});
|
||||
|
||||
// Load and evaluate the Rhai script
|
||||
let script_path = Path::new("examples/governance_rhai/governance.rhai");
|
||||
let script = fs::read_to_string(script_path)?;
|
||||
|
||||
match engine.eval::<()>(&script) {
|
||||
Ok(_) => println!("Script executed successfully!"),
|
||||
Err(e) => eprintln!("Script execution failed: {}", e),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
85
heromodels/examples/governance_rhai/governance.rhai
Normal file
85
heromodels/examples/governance_rhai/governance.rhai
Normal file
@@ -0,0 +1,85 @@
|
||||
// Get the database instance
|
||||
let db = get_db();
|
||||
|
||||
// Create a new proposal with auto-generated ID (pass 0 for auto-generated ID)
|
||||
let proposal = create_proposal(0, "user_creator_123", "Community Fund Allocation for Q3",
|
||||
"Proposal to allocate funds for community projects in the third quarter.");
|
||||
|
||||
print("Created Proposal: '" + get_title(proposal) + "' (ID: " + get_id(proposal) + ")");
|
||||
print("Status: " + get_status(proposal) + ", Vote Status: " + get_vote_status(proposal));
|
||||
|
||||
// Add vote options
|
||||
let proposal_with_options = add_option_to_proposal(proposal, 1, "Approve Allocation");
|
||||
proposal_with_options = add_option_to_proposal(proposal_with_options, 2, "Reject Allocation");
|
||||
proposal_with_options = add_option_to_proposal(proposal_with_options, 3, "Abstain");
|
||||
|
||||
print("\nAdded Vote Options:");
|
||||
let option_count = get_option_count(proposal_with_options);
|
||||
for i in range(0, option_count) {
|
||||
let option = get_option_at(proposal_with_options, i);
|
||||
print("- Option ID: " + i + ", Text: '" + get_option_text(option) + "', Votes: " + get_option_votes(option));
|
||||
}
|
||||
|
||||
// Save the proposal to the database
|
||||
save_proposal(db, proposal_with_options);
|
||||
print("\nProposal saved to database");
|
||||
|
||||
// Simulate casting votes
|
||||
print("\nSimulating Votes...");
|
||||
// User 1 votes for 'Approve Allocation' with 100 shares (with explicit ballot ID)
|
||||
let proposal_with_votes = cast_vote_on_proposal(proposal_with_options, 101, 1, 1, 100);
|
||||
// User 2 votes for 'Reject Allocation' with 50 shares (with explicit ballot ID)
|
||||
proposal_with_votes = cast_vote_on_proposal(proposal_with_votes, 102, 2, 2, 50);
|
||||
// User 3 votes for 'Approve Allocation' with 75 shares (with auto-generated ballot ID)
|
||||
proposal_with_votes = cast_vote_on_proposal(proposal_with_votes, 0, 3, 1, 75);
|
||||
// User 4 abstains with 20 shares (with auto-generated ballot ID)
|
||||
proposal_with_votes = cast_vote_on_proposal(proposal_with_votes, 0, 4, 3, 20);
|
||||
|
||||
print("\nVote Counts After Simulation:");
|
||||
option_count = get_option_count(proposal_with_votes);
|
||||
for i in range(0, option_count) {
|
||||
let option = get_option_at(proposal_with_votes, i);
|
||||
print("- Option ID: " + i + ", Text: '" + get_option_text(option) + "', Votes: " + get_option_votes(option));
|
||||
}
|
||||
|
||||
print("\nBallots Cast:");
|
||||
let ballot_count = get_ballot_count(proposal_with_votes);
|
||||
for i in range(0, ballot_count) {
|
||||
let ballot = get_ballot_at(proposal_with_votes, i);
|
||||
print("- Ballot ID: " + i + ", User ID: " + get_ballot_user_id(ballot) +
|
||||
", Option ID: " + get_ballot_option_id(ballot) + ", Shares: " + get_ballot_shares(ballot));
|
||||
}
|
||||
|
||||
// Change proposal status
|
||||
let active_proposal = change_proposal_status(proposal_with_votes, "Active");
|
||||
print("\nChanged Proposal Status to: " + get_status(active_proposal));
|
||||
|
||||
// Simulate closing the vote
|
||||
let closed_proposal = change_vote_event_status(active_proposal, "Closed");
|
||||
print("Changed Vote Event Status to: " + get_vote_status(closed_proposal));
|
||||
|
||||
// Final proposal state
|
||||
print("\nFinal Proposal State:");
|
||||
print("Title: '" + get_title(closed_proposal) + "'");
|
||||
print("Status: " + get_status(closed_proposal));
|
||||
print("Vote Status: " + get_vote_status(closed_proposal));
|
||||
print("Options:");
|
||||
option_count = get_option_count(closed_proposal);
|
||||
for i in range(0, option_count) {
|
||||
let option = get_option_at(closed_proposal, i);
|
||||
print(" - " + i + ": " + get_option_text(option) + " (Votes: " + get_option_votes(option) + ")");
|
||||
}
|
||||
print("Total Ballots: " + get_ballot_count(closed_proposal));
|
||||
|
||||
// Get all proposals from the database
|
||||
let all_proposals = get_all_proposals(db);
|
||||
print("\nTotal Proposals in Database: " + all_proposals.len());
|
||||
for proposal in all_proposals {
|
||||
print("Proposal ID: " + get_id(proposal) + ", Title: '" + get_title(proposal) + "'");
|
||||
}
|
||||
|
||||
// Delete a proposal
|
||||
delete_proposal_by_id(db, 1);
|
||||
print("\nDeleted proposal with ID 1");
|
||||
|
||||
print("\nGovernance Proposal Example Finished.");
|
366
heromodels/examples/governance_rhai_client/example.rs
Normal file
366
heromodels/examples/governance_rhai_client/example.rs
Normal file
@@ -0,0 +1,366 @@
|
||||
use heromodels::db::hero::OurDB;
|
||||
use heromodels::models::governance::{Proposal, ProposalStatus, VoteEventStatus, VoteOption, Ballot};
|
||||
use rhai::Engine;
|
||||
use rhai_wrapper::wrap_vec_return;
|
||||
use std::sync::Arc;
|
||||
use chrono::{Utc, Duration};
|
||||
use rhai_client_macros::rhai;
|
||||
|
||||
// Define the functions we want to expose to Rhai
|
||||
// We'll only use the #[rhai] attribute on functions with simple types
|
||||
|
||||
// Create a proposal (returns a complex type, but takes simple parameters)
|
||||
fn create_proposal(id: i64, creator_id: String, title: String, description: String) -> Proposal {
|
||||
let start_date = Utc::now();
|
||||
let end_date = start_date + Duration::days(14);
|
||||
Proposal::new(id as u32, creator_id, title, description, start_date, end_date)
|
||||
}
|
||||
|
||||
// Getter functions for Proposal properties
|
||||
fn get_title(proposal: &Proposal) -> String {
|
||||
proposal.title.clone()
|
||||
}
|
||||
|
||||
fn get_description(proposal: &Proposal) -> String {
|
||||
proposal.description.clone()
|
||||
}
|
||||
|
||||
fn get_creator_id(proposal: &Proposal) -> String {
|
||||
proposal.creator_id.clone()
|
||||
}
|
||||
|
||||
fn get_id(proposal: &Proposal) -> i64 {
|
||||
proposal.base_data.id as i64
|
||||
}
|
||||
|
||||
fn get_status(proposal: &Proposal) -> String {
|
||||
format!("{:?}", proposal.status)
|
||||
}
|
||||
|
||||
fn get_vote_status(proposal: &Proposal) -> String {
|
||||
format!("{:?}", proposal.vote_status)
|
||||
}
|
||||
|
||||
// Functions that operate on Proposal objects
|
||||
fn add_option_to_proposal(proposal: Proposal, option_id: i64, option_text: String) -> Proposal {
|
||||
proposal.add_option(option_id as u8, option_text)
|
||||
}
|
||||
|
||||
fn cast_vote_on_proposal(proposal: Proposal, ballot_id: i64, user_id: i64, option_id: i64, shares: i64) -> Proposal {
|
||||
proposal.cast_vote(ballot_id as u32, user_id as u32, option_id as u8, shares)
|
||||
}
|
||||
|
||||
fn change_proposal_status(proposal: Proposal, status_str: String) -> Proposal {
|
||||
let new_status = match status_str.as_str() {
|
||||
"Draft" => ProposalStatus::Draft,
|
||||
"Active" => ProposalStatus::Active,
|
||||
"Approved" => ProposalStatus::Approved,
|
||||
"Rejected" => ProposalStatus::Rejected,
|
||||
"Cancelled" => ProposalStatus::Cancelled,
|
||||
_ => ProposalStatus::Draft,
|
||||
};
|
||||
proposal.change_proposal_status(new_status)
|
||||
}
|
||||
|
||||
fn change_vote_event_status(proposal: Proposal, status_str: String) -> Proposal {
|
||||
let new_status = match status_str.as_str() {
|
||||
"Open" => VoteEventStatus::Open,
|
||||
"Closed" => VoteEventStatus::Closed,
|
||||
"Cancelled" => VoteEventStatus::Cancelled,
|
||||
_ => VoteEventStatus::Open,
|
||||
};
|
||||
proposal.change_vote_event_status(new_status)
|
||||
}
|
||||
|
||||
// Functions for accessing proposal options and ballots
|
||||
fn get_option_count(proposal: &Proposal) -> i64 {
|
||||
proposal.options.len() as i64
|
||||
}
|
||||
|
||||
fn get_option_at(proposal: &Proposal, index: i64) -> VoteOption {
|
||||
if index >= 0 && index < proposal.options.len() as i64 {
|
||||
proposal.options[index as usize].clone()
|
||||
} else {
|
||||
VoteOption::new(0, "Invalid Option")
|
||||
}
|
||||
}
|
||||
|
||||
fn get_option_text(option: &VoteOption) -> String {
|
||||
option.text.clone()
|
||||
}
|
||||
|
||||
fn get_option_votes(option: &VoteOption) -> i64 {
|
||||
option.count
|
||||
}
|
||||
|
||||
fn get_ballot_count(proposal: &Proposal) -> i64 {
|
||||
proposal.ballots.len() as i64
|
||||
}
|
||||
|
||||
fn get_ballot_at(proposal: &Proposal, index: i64) -> Ballot {
|
||||
if index >= 0 && index < proposal.ballots.len() as i64 {
|
||||
proposal.ballots[index as usize].clone()
|
||||
} else {
|
||||
Ballot::new(0, 0, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_ballot_user_id(ballot: &Ballot) -> i64 {
|
||||
ballot.user_id as i64
|
||||
}
|
||||
|
||||
fn get_ballot_option_id(ballot: &Ballot) -> i64 {
|
||||
ballot.vote_option_id as i64
|
||||
}
|
||||
|
||||
fn get_ballot_shares(ballot: &Ballot) -> i64 {
|
||||
ballot.shares_count
|
||||
}
|
||||
|
||||
// Simple functions that we can use with the #[rhai] attribute
|
||||
#[rhai]
|
||||
fn create_proposal_wrapper(id: i64, creator_id: String, title: String, description: String) -> String {
|
||||
let proposal = create_proposal(id, creator_id, title, description);
|
||||
format!("Created proposal with ID: {}", proposal.base_data.id)
|
||||
}
|
||||
|
||||
#[rhai]
|
||||
fn add_option_wrapper(id: i64, option_id: i64, option_text: String) -> String {
|
||||
let proposal = create_proposal(id, "user".to_string(), "title".to_string(), "description".to_string());
|
||||
let updated = add_option_to_proposal(proposal, option_id, option_text.clone());
|
||||
format!("Added option '{}' to proposal {}", option_text, id)
|
||||
}
|
||||
|
||||
// Database operations
|
||||
fn save_proposal(_db: Arc<OurDB>, proposal: Proposal) {
|
||||
println!("Proposal saved: {}", proposal.title);
|
||||
}
|
||||
|
||||
fn get_all_proposals(_db: Arc<OurDB>) -> Vec<Proposal> {
|
||||
// In a real implementation, this would retrieve all proposals from the database
|
||||
let start_date = Utc::now();
|
||||
let end_date = start_date + Duration::days(14);
|
||||
vec![
|
||||
Proposal::new(1, "Creator 1", "Proposal 1", "Description 1", start_date, end_date),
|
||||
Proposal::new(2, "Creator 2", "Proposal 2", "Description 2", start_date, end_date)
|
||||
]
|
||||
}
|
||||
|
||||
fn delete_proposal_by_id(_db: Arc<OurDB>, id: i64) {
|
||||
// In a real implementation, this would delete the proposal from the database
|
||||
println!("Proposal deleted with ID: {}", id);
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize Rhai engine
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Initialize database
|
||||
let db = Arc::new(OurDB::new("temp_governance_db", true).expect("Failed to create database"));
|
||||
|
||||
// Register the Proposal type with Rhai
|
||||
// This function is generated by the #[rhai_model_export] attribute
|
||||
Proposal::register_rhai_bindings_for_proposal(&mut engine, db.clone());
|
||||
|
||||
// Register the Ballot type with Rhai
|
||||
Ballot::register_rhai_bindings_for_ballot(&mut engine, db.clone());
|
||||
|
||||
// Create a clone of db for use in the get_db function
|
||||
let db_for_get_db = db.clone();
|
||||
|
||||
// Register a function to get the database instance
|
||||
engine.register_fn("get_db", move || db_for_get_db.clone());
|
||||
|
||||
// Register builder functions for Proposal and related types
|
||||
engine.register_fn("create_proposal", |id: i64, creator_id: String, title: String, description: String| {
|
||||
let start_date = Utc::now();
|
||||
let end_date = start_date + Duration::days(14);
|
||||
Proposal::new(id as u32, creator_id, title, description, start_date, end_date)
|
||||
});
|
||||
|
||||
engine.register_fn("create_vote_option", |id: i64, text: String| {
|
||||
VoteOption::new(id as u8, text)
|
||||
});
|
||||
|
||||
engine.register_fn("create_ballot", |id: i64, user_id: i64, vote_option_id: i64, shares_count: i64| {
|
||||
Ballot::new(id as u32, user_id as u32, vote_option_id as u8, shares_count)
|
||||
});
|
||||
|
||||
// Register getter and setter methods for Proposal properties
|
||||
engine.register_fn("get_title", get_title);
|
||||
engine.register_fn("get_description", get_description);
|
||||
engine.register_fn("get_creator_id", get_creator_id);
|
||||
engine.register_fn("get_id", get_id);
|
||||
engine.register_fn("get_status", get_status);
|
||||
engine.register_fn("get_vote_status", get_vote_status);
|
||||
|
||||
// Register methods for proposal operations
|
||||
engine.register_fn("add_option_to_proposal", add_option_to_proposal);
|
||||
engine.register_fn("cast_vote_on_proposal", cast_vote_on_proposal);
|
||||
engine.register_fn("change_proposal_status", change_proposal_status);
|
||||
engine.register_fn("change_vote_event_status", change_vote_event_status);
|
||||
|
||||
// Register functions for database operations
|
||||
engine.register_fn("save_proposal", save_proposal);
|
||||
|
||||
engine.register_fn("get_proposal_by_id", |_db: Arc<OurDB>, id: i64| -> Proposal {
|
||||
// In a real implementation, this would retrieve the proposal from the database
|
||||
let start_date = Utc::now();
|
||||
let end_date = start_date + Duration::days(14);
|
||||
Proposal::new(id as u32, "Retrieved Creator", "Retrieved Proposal", "Retrieved Description", start_date, end_date)
|
||||
});
|
||||
|
||||
// Register a function to check if a proposal exists
|
||||
engine.register_fn("proposal_exists", |_db: Arc<OurDB>, id: i64| -> bool {
|
||||
// In a real implementation, this would check if the proposal exists in the database
|
||||
id == 1 || id == 2
|
||||
});
|
||||
|
||||
// Register the function with the wrap_vec_return macro
|
||||
engine.register_fn("get_all_proposals", wrap_vec_return!(get_all_proposals, Arc<OurDB> => Proposal));
|
||||
|
||||
engine.register_fn("delete_proposal_by_id", delete_proposal_by_id);
|
||||
|
||||
// Register helper functions for accessing proposal options and ballots
|
||||
engine.register_fn("get_option_count", get_option_count);
|
||||
engine.register_fn("get_option_at", get_option_at);
|
||||
engine.register_fn("get_option_text", get_option_text);
|
||||
engine.register_fn("get_option_votes", get_option_votes);
|
||||
engine.register_fn("get_ballot_count", get_ballot_count);
|
||||
engine.register_fn("get_ballot_at", get_ballot_at);
|
||||
engine.register_fn("get_ballot_user_id", get_ballot_user_id);
|
||||
engine.register_fn("get_ballot_option_id", get_ballot_option_id);
|
||||
engine.register_fn("get_ballot_shares", get_ballot_shares);
|
||||
|
||||
// Register our wrapper functions that use the #[rhai] attribute
|
||||
engine.register_fn("create_proposal_wrapper", create_proposal_wrapper);
|
||||
engine.register_fn("add_option_wrapper", add_option_wrapper);
|
||||
|
||||
// Now instead of loading and evaluating a Rhai script, we'll use direct function calls
|
||||
// to implement the same functionality
|
||||
|
||||
// Use the database instance
|
||||
|
||||
// Create a new proposal
|
||||
let proposal = create_proposal(1,
|
||||
"user_creator_123".to_string(),
|
||||
"Community Fund Allocation for Q3".to_string(),
|
||||
"Proposal to allocate funds for community projects in the third quarter.".to_string());
|
||||
|
||||
println!("Created Proposal: '{}' (ID: {})",
|
||||
get_title(&proposal),
|
||||
get_id(&proposal));
|
||||
println!("Status: {}, Vote Status: {}",
|
||||
get_status(&proposal),
|
||||
get_vote_status(&proposal));
|
||||
|
||||
// Add vote options
|
||||
let mut proposal_with_options = add_option_to_proposal(
|
||||
proposal, 1, "Approve Allocation".to_string());
|
||||
proposal_with_options = add_option_to_proposal(
|
||||
proposal_with_options, 2, "Reject Allocation".to_string());
|
||||
proposal_with_options = add_option_to_proposal(
|
||||
proposal_with_options, 3, "Abstain".to_string());
|
||||
|
||||
println!("\nAdded Vote Options:");
|
||||
let option_count = get_option_count(&proposal_with_options);
|
||||
for i in 0..option_count {
|
||||
let option = get_option_at(&proposal_with_options, i);
|
||||
println!("- Option ID: {}, Text: '{}', Votes: {}",
|
||||
i, get_option_text(&option),
|
||||
get_option_votes(&option));
|
||||
}
|
||||
|
||||
// Save the proposal to the database
|
||||
save_proposal(db.clone(), proposal_with_options.clone());
|
||||
println!("\nProposal saved to database");
|
||||
|
||||
// Simulate casting votes
|
||||
println!("\nSimulating Votes...");
|
||||
// User 1 votes for 'Approve Allocation' with 100 shares
|
||||
let mut proposal_with_votes = cast_vote_on_proposal(
|
||||
proposal_with_options, 101, 1, 1, 100);
|
||||
// User 2 votes for 'Reject Allocation' with 50 shares
|
||||
proposal_with_votes = cast_vote_on_proposal(
|
||||
proposal_with_votes, 102, 2, 2, 50);
|
||||
// User 3 votes for 'Approve Allocation' with 75 shares
|
||||
proposal_with_votes = cast_vote_on_proposal(
|
||||
proposal_with_votes, 103, 3, 1, 75);
|
||||
// User 4 abstains with 20 shares
|
||||
proposal_with_votes = cast_vote_on_proposal(
|
||||
proposal_with_votes, 104, 4, 3, 20);
|
||||
|
||||
println!("\nVote Counts After Simulation:");
|
||||
let option_count = get_option_count(&proposal_with_votes);
|
||||
for i in 0..option_count {
|
||||
let option = get_option_at(&proposal_with_votes, i);
|
||||
println!("- Option ID: {}, Text: '{}', Votes: {}",
|
||||
i, get_option_text(&option),
|
||||
get_option_votes(&option));
|
||||
}
|
||||
|
||||
println!("\nBallots Cast:");
|
||||
let ballot_count = get_ballot_count(&proposal_with_votes);
|
||||
for i in 0..ballot_count {
|
||||
let ballot = get_ballot_at(&proposal_with_votes, i);
|
||||
println!("- Ballot ID: {}, User ID: {}, Option ID: {}, Shares: {}",
|
||||
i, get_ballot_user_id(&ballot),
|
||||
get_ballot_option_id(&ballot),
|
||||
get_ballot_shares(&ballot));
|
||||
}
|
||||
|
||||
// Change proposal status
|
||||
let active_proposal = change_proposal_status(
|
||||
proposal_with_votes, "Active".to_string());
|
||||
println!("\nChanged Proposal Status to: {}",
|
||||
get_status(&active_proposal));
|
||||
|
||||
// Simulate closing the vote
|
||||
let closed_proposal = change_vote_event_status(
|
||||
active_proposal, "Closed".to_string());
|
||||
println!("Changed Vote Event Status to: {}",
|
||||
get_vote_status(&closed_proposal));
|
||||
|
||||
// Final proposal state
|
||||
println!("\nFinal Proposal State:");
|
||||
println!("Title: '{}'", get_title(&closed_proposal));
|
||||
println!("Status: {}", get_status(&closed_proposal));
|
||||
println!("Vote Status: {}", get_vote_status(&closed_proposal));
|
||||
println!("Options:");
|
||||
let option_count = get_option_count(&closed_proposal);
|
||||
for i in 0..option_count {
|
||||
let option = get_option_at(&closed_proposal, i);
|
||||
println!(" - {}: {} (Votes: {})",
|
||||
i, get_option_text(&option),
|
||||
get_option_votes(&option));
|
||||
}
|
||||
println!("Total Ballots: {}", get_ballot_count(&closed_proposal));
|
||||
|
||||
// Get all proposals from the database
|
||||
let all_proposals = get_all_proposals(db.clone());
|
||||
println!("\nTotal Proposals in Database: {}", all_proposals.len());
|
||||
for proposal in all_proposals {
|
||||
println!("Proposal ID: {}, Title: '{}'",
|
||||
get_id(&proposal),
|
||||
get_title(&proposal));
|
||||
}
|
||||
|
||||
// Delete a proposal
|
||||
delete_proposal_by_id(db.clone(), 1);
|
||||
println!("\nDeleted proposal with ID 1");
|
||||
|
||||
// Demonstrate the use of Rhai client functions for simple types
|
||||
println!("\nUsing Rhai client functions for simple types:");
|
||||
let create_result = create_proposal_wrapper_rhai_client(&engine, 2,
|
||||
"rhai_user".to_string(),
|
||||
"Rhai Proposal".to_string(),
|
||||
"This proposal was created using a Rhai client function".to_string());
|
||||
println!("{}", create_result);
|
||||
|
||||
let add_option_result = add_option_wrapper_rhai_client(&engine, 2, 4, "Rhai Option".to_string());
|
||||
println!("{}", add_option_result);
|
||||
|
||||
println!("\nGovernance Proposal Example Finished.");
|
||||
|
||||
Ok(())
|
||||
}
|
115
heromodels/examples/legal_contract_example.rs
Normal file
115
heromodels/examples/legal_contract_example.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use heromodels::models::legal::{
|
||||
Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus,
|
||||
};
|
||||
// If BaseModelData's touch method or new method isn't directly part of the public API
|
||||
// of heromodels crate root, we might need to import heromodels_core.
|
||||
// For now, assuming `contract.base_data.touch()` would work if BaseModelData has a public `touch` method.
|
||||
// Or, if `heromodels::models::BaseModelData` is the path.
|
||||
|
||||
// A helper for current timestamp (seconds since epoch)
|
||||
fn current_timestamp_secs() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("Demonstrating Legal Contract Model Usage");
|
||||
|
||||
// Create contract signers
|
||||
let signer1 = ContractSigner::new(
|
||||
"signer-uuid-alice-001".to_string(),
|
||||
"Alice Wonderland".to_string(),
|
||||
"alice@example.com".to_string(),
|
||||
)
|
||||
.status(SignerStatus::Pending)
|
||||
.comments("Awaiting Alice's review and signature.");
|
||||
|
||||
let signer2 = ContractSigner::new(
|
||||
"signer-uuid-bob-002".to_string(),
|
||||
"Bob The Builder".to_string(),
|
||||
"bob@example.com".to_string(),
|
||||
)
|
||||
.status(SignerStatus::Signed)
|
||||
.signed_at(current_timestamp_secs() - 86400) // Signed yesterday
|
||||
.comments("Bob has signed the agreement.");
|
||||
|
||||
// Create contract revisions
|
||||
let revision1 = ContractRevision::new(
|
||||
1,
|
||||
"Initial draft: This Service Agreement outlines the terms...".to_string(),
|
||||
current_timestamp_secs() - (86400 * 2), // 2 days ago
|
||||
"user-uuid-creator-charlie".to_string(),
|
||||
)
|
||||
.comments("Version 1.0 - Initial draft for review.");
|
||||
|
||||
let revision2 = ContractRevision::new(
|
||||
2,
|
||||
"Updated draft: Added clause 5.b regarding data privacy...".to_string(),
|
||||
current_timestamp_secs() - 86400, // 1 day ago
|
||||
"user-uuid-editor-diana".to_string(),
|
||||
)
|
||||
.comments("Version 2.0 - Incorporated feedback from legal team.");
|
||||
|
||||
// Create a new contract
|
||||
// base_id (u32 for BaseModelData) and contract_id (String for UUID)
|
||||
let mut contract = Contract::new(101, "contract-uuid-main-789".to_string())
|
||||
.title("Master Service Agreement (MSA) - Q3 2025")
|
||||
.description("Agreement for ongoing IT support services between TechSolutions Inc. and ClientCorp LLC.")
|
||||
.contract_type("MSA".to_string())
|
||||
.status(ContractStatus::PendingSignatures)
|
||||
.created_by("user-uuid-admin-eve".to_string())
|
||||
.terms_and_conditions("The full terms are detailed in the attached PDF, referenced as 'MSA_Q3_2025_Full.pdf'. This string can hold markdown or JSON summary.")
|
||||
.start_date(current_timestamp_secs() + (86400 * 7)) // Starts in 7 days
|
||||
.end_date(current_timestamp_secs() + (86400 * (365 + 7))) // Ends in 1 year + 7 days
|
||||
.renewal_period_days(30)
|
||||
.next_renewal_date(current_timestamp_secs() + (86400 * (365 + 7 - 30))) // Approx. 30 days before end_date
|
||||
.current_version(2)
|
||||
.add_signer(signer1.clone())
|
||||
.add_signer(signer2.clone())
|
||||
.add_revision(revision1.clone())
|
||||
.add_revision(revision2.clone());
|
||||
|
||||
// The `#[model]` derive handles `created_at` and `updated_at` in `base_data`.
|
||||
// `base_data.touch()` might be called internally by setters or needs explicit call if fields are set directly.
|
||||
// For builder pattern, the final state of `base_data.updated_at` reflects the time of the last builder call if `touch()` is implicit.
|
||||
// If not, one might call `contract.base_data.touch()` after building.
|
||||
|
||||
println!("\n--- Initial Contract Details ---");
|
||||
println!("{:#?}", contract);
|
||||
|
||||
// Simulate a status change and signing
|
||||
contract.set_status(ContractStatus::Signed); // This should call base_data.touch()
|
||||
contract = contract.last_signed_date(current_timestamp_secs());
|
||||
// If set_status doesn't touch, and last_signed_date is not a builder method that touches:
|
||||
// contract.base_data.touch(); // Manually update timestamp if needed after direct field manipulation
|
||||
|
||||
println!("\n--- Contract Details After Signing ---");
|
||||
println!("{:#?}", contract);
|
||||
|
||||
println!("\n--- Accessing Specific Fields ---");
|
||||
println!("Contract Title: {}", contract.title);
|
||||
println!("Contract Status: {:?}", contract.status);
|
||||
println!("Contract ID (UUID): {}", contract.contract_id);
|
||||
println!("Base Model ID (u32): {}", contract.base_data.id); // From BaseModelData
|
||||
println!("Created At (timestamp): {}", contract.base_data.created_at); // From BaseModelData
|
||||
println!("Updated At (timestamp): {}", contract.base_data.modified_at); // From BaseModelData
|
||||
|
||||
if let Some(first_signer_details) = contract.signers.first() {
|
||||
println!("\nFirst Signer: {} ({})", first_signer_details.name, first_signer_details.email);
|
||||
println!(" Status: {:?}", first_signer_details.status);
|
||||
if let Some(signed_time) = first_signer_details.signed_at {
|
||||
println!(" Signed At: {}", signed_time);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(latest_rev) = contract.revisions.iter().max_by_key(|r| r.version) {
|
||||
println!("\nLatest Revision (v{}):", latest_rev.version);
|
||||
println!(" Content Snippet: {:.60}...", latest_rev.content);
|
||||
println!(" Created By: {}", latest_rev.created_by);
|
||||
println!(" Revision Created At: {}", latest_rev.created_at);
|
||||
}
|
||||
|
||||
println!("\nLegal Contract Model demonstration complete.");
|
||||
}
|
44
heromodels/examples/legal_rhai/example.rs
Normal file
44
heromodels/examples/legal_rhai/example.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use heromodels::db::hero::OurDB;
|
||||
use heromodels::models::legal::register_legal_rhai_module;
|
||||
use rhai::Engine;
|
||||
use std::sync::Arc;
|
||||
use std::{fs, path::Path};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize Rhai engine
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Initialize database with OurDB
|
||||
// Using a temporary/in-memory database for the example (creates files in current dir)
|
||||
let db_name = "temp_legal_rhai_db";
|
||||
let db = Arc::new(OurDB::new(db_name, true).expect("Failed to create database"));
|
||||
|
||||
// Register legal Rhai module functions
|
||||
register_legal_rhai_module(&mut engine, db.clone());
|
||||
|
||||
// Load and evaluate the Rhai script
|
||||
let script_path_str = "examples/legal_rhai/legal.rhai";
|
||||
let script_path = Path::new(script_path_str);
|
||||
|
||||
if !script_path.exists() {
|
||||
eprintln!("Error: Rhai script not found at {}", script_path_str);
|
||||
eprintln!("Please ensure the script 'legal.rhai' exists in the 'examples/legal_rhai/' directory.");
|
||||
return Err(Box::new(std::io::Error::new(std::io::ErrorKind::NotFound, format!("Rhai script not found: {}", script_path_str))));
|
||||
}
|
||||
|
||||
println!("Executing Rhai script: {}", script_path_str);
|
||||
let script = fs::read_to_string(script_path)?;
|
||||
|
||||
match engine.eval::<()>(&script) {
|
||||
Ok(_) => println!("\nRhai script executed successfully!"),
|
||||
Err(e) => {
|
||||
eprintln!("\nRhai script execution failed: {}\nDetails: {:#?}", e, e);
|
||||
// No explicit cleanup in this example, similar to flow_rhai_example
|
||||
return Err(e.into()); // Propagate the Rhai error
|
||||
}
|
||||
}
|
||||
|
||||
// No explicit cleanup in this example, similar to flow_rhai_example
|
||||
|
||||
Ok(())
|
||||
}
|
119
heromodels/examples/legal_rhai/legal.rhai
Normal file
119
heromodels/examples/legal_rhai/legal.rhai
Normal file
@@ -0,0 +1,119 @@
|
||||
// heromodels - Legal Module Rhai Example
|
||||
print("Hero Models - Legal Rhai Example");
|
||||
print("===============================");
|
||||
|
||||
// Helper to format Option<String> (Dynamic in Rhai: String or ()) for printing
|
||||
fn format_optional_string(val, placeholder) {
|
||||
if val == () {
|
||||
placeholder
|
||||
} else {
|
||||
val
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to format Option<i64> (Dynamic in Rhai: i64 or ()) for printing
|
||||
fn format_optional_int(val, placeholder) {
|
||||
if val == () {
|
||||
placeholder
|
||||
} else {
|
||||
"" + val // Convert int to string for concatenation
|
||||
}
|
||||
}
|
||||
|
||||
print("DB instance will be implicitly passed to DB functions.");
|
||||
|
||||
// --- Using Enum Constants ---
|
||||
print(`\n--- Enum Constants ---`);
|
||||
print(`ContractStatus Draft: ${ContractStatusConstants::Draft}`);
|
||||
print(`ContractStatus Active: ${ContractStatusConstants::Active}`);
|
||||
print(`SignerStatus Pending: ${SignerStatusConstants::Pending}`);
|
||||
print(`SignerStatus Signed: ${SignerStatusConstants::Signed}`);
|
||||
|
||||
// --- Test ContractSigner Model ---
|
||||
print("\n--- Testing ContractSigner Model ---");
|
||||
let signer1_id = "signer-uuid-001";
|
||||
let signer1 = new_contract_signer(signer1_id, "Alice Wonderland", "alice@example.com")
|
||||
.status(SignerStatusConstants::Pending)
|
||||
.comments("Alice is the primary signatory.");
|
||||
|
||||
print(`Signer 1 ID: ${signer1.id}, Name: ${signer1.name}, Email: ${signer1.email}`);
|
||||
print(`Signer 1 Status: ${signer1.status}, Comments: ${format_optional_string(signer1.comments, "N/A")}`);
|
||||
print(`Signer 1 Signed At: ${format_optional_int(signer1.signed_at, "Not signed")}`);
|
||||
|
||||
let signer2_id = "signer-uuid-002";
|
||||
let signer2 = new_contract_signer(signer2_id, "Bob The Builder", "bob@example.com")
|
||||
.status(SignerStatusConstants::Signed)
|
||||
.signed_at(1678886400) // Example timestamp
|
||||
.comments("Bob has already signed.");
|
||||
|
||||
print(`Signer 2 ID: ${signer2.id}, Name: ${signer2.name}, Status: ${signer2.status}, Signed At: ${format_optional_int(signer2.signed_at, "N/A")}`);
|
||||
|
||||
// --- Test ContractRevision Model ---
|
||||
print("\n--- Testing ContractRevision Model ---");
|
||||
let revision1_version = 1;
|
||||
let revision1 = new_contract_revision(revision1_version, "Initial draft content for the agreement, version 1.", 1678880000, "user-admin-01")
|
||||
.comments("First version of the contract.");
|
||||
|
||||
print(`Revision 1 Version: ${revision1.version}, Content: '${revision1.content}', Created At: ${revision1.created_at}, By: ${revision1.created_by}`);
|
||||
print(`Revision 1 Comments: ${format_optional_string(revision1.comments, "N/A")}`);
|
||||
|
||||
let revision2_version = 2;
|
||||
let revision2 = new_contract_revision(revision2_version, "Updated content with new clauses, version 2.", 1678882200, "user-legal-02");
|
||||
|
||||
// --- Test Contract Model ---
|
||||
print("\n--- Testing Contract Model ---");
|
||||
let contract1_base_id = 101;
|
||||
let contract1_uuid = "contract-uuid-xyz-001";
|
||||
|
||||
print(`Creating a new contract (ID: ${contract1_base_id}, UUID: ${contract1_uuid})...`);
|
||||
let contract1 = new_contract(contract1_base_id, contract1_uuid)
|
||||
.title("Master Service Agreement")
|
||||
.description("MSA between ACME Corp and Client Inc.")
|
||||
.contract_type("Services")
|
||||
.status(ContractStatusConstants::Draft)
|
||||
.created_by("user-admin-01")
|
||||
.terms_and_conditions("Standard terms and conditions apply. See Appendix A.")
|
||||
.start_date(1678900000)
|
||||
.current_version(revision1.version)
|
||||
.add_signer(signer1)
|
||||
.add_revision(revision1);
|
||||
|
||||
print(`Contract 1 Title: ${contract1.title}, Status: ${contract1.status}`);
|
||||
print(`Contract 1 Signers: ${contract1.signers.len()}, Revisions: ${contract1.revisions.len()}`);
|
||||
|
||||
// Add more data
|
||||
contract1 = contract1.add_signer(signer2).add_revision(revision2).current_version(revision2.version);
|
||||
print(`Contract 1 Updated Signers: ${contract1.signers.len()}, Revisions: ${contract1.revisions.len()}, Current Version: ${contract1.current_version}`);
|
||||
|
||||
// Save the contract to the database
|
||||
print("Saving contract1 to database...");
|
||||
set_contract(contract1);
|
||||
print("Contract1 saved.");
|
||||
|
||||
// Retrieve the contract
|
||||
print(`Retrieving contract by ID (${contract1_base_id})...`);
|
||||
let retrieved_contract = get_contract_by_id(contract1_base_id);
|
||||
print(`Retrieved Contract: ${retrieved_contract.title}, Status: ${retrieved_contract.status}`);
|
||||
print(`Retrieved Contract Signers: ${retrieved_contract.signers.len()}, Revisions: ${retrieved_contract.revisions.len()}`);
|
||||
if retrieved_contract.signers.len() > 0 {
|
||||
print(`First signer of retrieved contract: ${retrieved_contract.signers[0].name}`);
|
||||
}
|
||||
if retrieved_contract.revisions.len() > 0 {
|
||||
print(`First revision content of retrieved contract: '${retrieved_contract.revisions[0].content}'`);
|
||||
}
|
||||
|
||||
// --- Test updating a Contract ---
|
||||
print("\n--- Testing Update for Contract ---");
|
||||
let updated_contract = retrieved_contract
|
||||
.status(ContractStatusConstants::Active)
|
||||
.end_date(1700000000)
|
||||
.description("MSA (Active) between ACME Corp and Client Inc. with new addendum.");
|
||||
|
||||
print(`Updated Contract - Title: ${updated_contract.title}, Status: ${updated_contract.status}, End Date: ${format_optional_int(updated_contract.end_date, "N/A")}`);
|
||||
set_contract(updated_contract); // Save updated
|
||||
print("Updated Contract saved.");
|
||||
|
||||
let final_retrieved_contract = get_contract_by_id(contract1_base_id);
|
||||
print(`Final Retrieved Contract - Status: ${final_retrieved_contract.status}, Description: '${final_retrieved_contract.description}'`);
|
||||
|
||||
print("\nLegal Rhai example script finished.");
|
@@ -1,3 +1,4 @@
|
||||
use heromodels::db::{Collection, Db};
|
||||
use heromodels_core::{BaseModelData, Model};
|
||||
use heromodels_derive::model;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -33,26 +34,54 @@ fn main() {
|
||||
println!("Hero Models - Model Macro Example");
|
||||
println!("=================================");
|
||||
|
||||
// Create a new DB instance, reset before every run
|
||||
let db_path = "/tmp/ourdb_model_macro_example";
|
||||
let db = heromodels::db::hero::OurDB::new(db_path, true).expect("Can create DB");
|
||||
|
||||
// Example usage of the generated implementations
|
||||
println!("SimpleUser DB Prefix: {}", SimpleUser::db_prefix());
|
||||
println!("CustomUser DB Prefix: {}", CustomUser::db_prefix());
|
||||
|
||||
let user = SimpleUser {
|
||||
base_data: BaseModelData::new(1),
|
||||
base_data: BaseModelData::new(), // ID will be auto-generated by OurDB
|
||||
login: "johndoe".to_string(),
|
||||
full_name: "John Doe".to_string(),
|
||||
};
|
||||
|
||||
let custom_user = CustomUser {
|
||||
base_data: BaseModelData::new(2),
|
||||
base_data: BaseModelData::new(), // ID will be auto-generated by OurDB
|
||||
login_name: "janesmith".to_string(),
|
||||
is_active: true,
|
||||
full_name: "Jane Smith".to_string(),
|
||||
};
|
||||
|
||||
println!("\nSimpleUser ID: {}", user.get_id());
|
||||
println!("SimpleUser DB Keys: {:?}", user.db_keys());
|
||||
println!("\nBefore saving - SimpleUser ID: {}", user.get_id());
|
||||
println!("Before saving - SimpleUser DB Keys: {:?}", user.db_keys());
|
||||
|
||||
println!("\nCustomUser ID: {}", custom_user.get_id());
|
||||
println!("CustomUser DB Keys: {:?}", custom_user.db_keys());
|
||||
println!("\nBefore saving - CustomUser ID: {}", custom_user.get_id());
|
||||
println!("Before saving - CustomUser DB Keys: {:?}", custom_user.db_keys());
|
||||
|
||||
// Save the models to the database
|
||||
let simple_collection = db.collection::<SimpleUser>().expect("can open simple user collection");
|
||||
let custom_collection = db.collection::<CustomUser>().expect("can open custom user collection");
|
||||
|
||||
let (user_id, saved_user) = simple_collection.set(&user).expect("can save simple user");
|
||||
let (custom_user_id, saved_custom_user) = custom_collection.set(&custom_user).expect("can save custom user");
|
||||
|
||||
println!("\nAfter saving - SimpleUser ID: {}", saved_user.get_id());
|
||||
println!("After saving - SimpleUser DB Keys: {:?}", saved_user.db_keys());
|
||||
println!("Returned SimpleUser ID: {}", user_id);
|
||||
|
||||
println!("\nAfter saving - CustomUser ID: {}", saved_custom_user.get_id());
|
||||
println!("After saving - CustomUser DB Keys: {:?}", saved_custom_user.db_keys());
|
||||
println!("Returned CustomUser ID: {}", custom_user_id);
|
||||
|
||||
// Verify that the IDs were auto-generated
|
||||
assert_eq!(saved_user.get_id(), user_id);
|
||||
assert_ne!(saved_user.get_id(), 0);
|
||||
assert_eq!(saved_custom_user.get_id(), custom_user_id);
|
||||
assert_ne!(saved_custom_user.get_id(), 0);
|
||||
|
||||
println!("\nExample finished. DB stored at {}", db_path);
|
||||
println!("To clean up, you can manually delete the directory: {}", db_path);
|
||||
}
|
||||
|
39
heromodels/examples/project_rhai/example.rs
Normal file
39
heromodels/examples/project_rhai/example.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use rhai::{Engine, EvalAltResult, Scope};
|
||||
use std::sync::Arc;
|
||||
use heromodels::db::hero::OurDB;
|
||||
use heromodels::models::projects::register_projects_rhai_module;
|
||||
use std::fs;
|
||||
|
||||
fn main() -> Result<(), Box<EvalAltResult>> {
|
||||
println!("Executing Rhai script: examples/project_rhai/project_test.rhai");
|
||||
|
||||
// Create a new Rhai engine
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Create an Arc<OurDB> instance
|
||||
let db_instance = Arc::new(OurDB::new(".", false).expect("Failed to create DB"));
|
||||
|
||||
// Register the projects module with the engine
|
||||
register_projects_rhai_module(&mut engine, Arc::clone(&db_instance));
|
||||
|
||||
// Read the Rhai script from file
|
||||
let script_path = "examples/project_rhai/project_test.rhai";
|
||||
let script_content = fs::read_to_string(script_path)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorSystem(format!("Cannot read script file: {}", script_path), e.into())))?;
|
||||
|
||||
// Create a new scope
|
||||
let mut scope = Scope::new();
|
||||
|
||||
// Execute the script
|
||||
match engine.run_with_scope(&mut scope, &script_content) {
|
||||
Ok(_) => {
|
||||
println!("Rhai script executed successfully!");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Rhai script execution failed: {}", e);
|
||||
println!("Details: {:?}", e);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
69
heromodels/examples/project_rhai/project_test.rhai
Normal file
69
heromodels/examples/project_rhai/project_test.rhai
Normal file
@@ -0,0 +1,69 @@
|
||||
// Test script for Project Rhai integration
|
||||
|
||||
print("--- Testing Project Rhai Integration ---");
|
||||
|
||||
// Create a new project
|
||||
let p1 = new_project()
|
||||
.name("Project Alpha")
|
||||
.description("This is the first test project.")
|
||||
.owner_id(101)
|
||||
.add_member_id(102)
|
||||
.add_member_id(103)
|
||||
.member_ids([201, 202, 203]) // Test setting multiple IDs
|
||||
.add_tag("important")
|
||||
.add_tag("rhai_test")
|
||||
.tags(["core", "feature_test"]) // Test setting multiple tags
|
||||
.status(Status::InProgress)
|
||||
.priority(Priority::High)
|
||||
.item_type(ItemType::Feature)
|
||||
.add_base_comment(1001);
|
||||
|
||||
print("Created project p1: " + p1);
|
||||
print("p1.name: " + p1.name);
|
||||
print("p1.description: " + p1.description);
|
||||
print("p1.owner_id: " + p1.owner_id);
|
||||
print("p1.member_ids: " + p1.member_ids);
|
||||
print("p1.tags: " + p1.tags);
|
||||
print(`p1.status: ${p1.status.to_string()}`);
|
||||
print(`p1.priority: ${p1.priority.to_string()}`);
|
||||
print(`p1.item_type: ${p1.item_type.to_string()}`);
|
||||
print("p1.id: " + p1.id);
|
||||
print("p1.created_at: " + p1.created_at);
|
||||
print("p1.modified_at: " + p1.modified_at);
|
||||
print("p1.comments: " + p1.comments);
|
||||
|
||||
// Save to DB
|
||||
try {
|
||||
set_project(p1);
|
||||
print("Project p1 saved successfully.");
|
||||
} catch (err) {
|
||||
print("Error saving project p1: " + err);
|
||||
}
|
||||
|
||||
// Retrieve from DB
|
||||
try {
|
||||
let retrieved_p1 = get_project_by_id(1);
|
||||
if retrieved_p1 != () { // Check if Some(project) was returned (None becomes '()')
|
||||
print("Retrieved project by ID 1: " + retrieved_p1);
|
||||
print("Retrieved project name: " + retrieved_p1.name);
|
||||
print("Retrieved project tags: " + retrieved_p1.tags);
|
||||
} else {
|
||||
print("Project with ID 1 not found.");
|
||||
}
|
||||
} catch (err) {
|
||||
print("Error retrieving project by ID 1: " + err);
|
||||
}
|
||||
|
||||
// Test non-existent project
|
||||
try {
|
||||
let non_existent_project = get_project_by_id(999);
|
||||
if non_existent_project != () {
|
||||
print("Error: Found non-existent project 999: " + non_existent_project);
|
||||
} else {
|
||||
print("Correctly did not find project with ID 999.");
|
||||
}
|
||||
} catch (err) {
|
||||
print("Error checking for non-existent project: " + err);
|
||||
}
|
||||
|
||||
print("--- Project Rhai Integration Test Complete ---");
|
9
heromodels/examples/project_rhai_wasm/.cargo/config.toml
Normal file
9
heromodels/examples/project_rhai_wasm/.cargo/config.toml
Normal file
@@ -0,0 +1,9 @@
|
||||
[build]
|
||||
# Set the default build target for this project
|
||||
target = "wasm32-unknown-unknown"
|
||||
|
||||
# Configuration for the wasm32-unknown-unknown target
|
||||
[target.wasm32-unknown-unknown]
|
||||
# Pass --cfg=wasm_js to rustc when compiling for this target.
|
||||
# This is required by the getrandom crate.
|
||||
rustflags = ["--cfg=wasm_js"] # For getrandom v0.3.x WASM support (required by rhai via ahash)
|
20
heromodels/examples/project_rhai_wasm/Cargo.toml
Normal file
20
heromodels/examples/project_rhai_wasm/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "project_rhai_wasm_example"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
heromodels = { path = "../..", features = ["rhai"] } # Match heromodels main crate
|
||||
wasm-bindgen = "0.2"
|
||||
web-sys = { version = "0.3", features = ["console"] }
|
||||
console_error_panic_hook = "0.1.7"
|
||||
js-sys = "0.3"
|
||||
getrandom = { version = "0.3.3", features = ["js"] } # Align with rhai's dependency
|
||||
|
||||
[profile.release]
|
||||
# Tell `rustc` to optimize for small code size.
|
||||
lto = true
|
||||
opt-level = 's'
|
52
heromodels/examples/project_rhai_wasm/index.html
Normal file
52
heromodels/examples/project_rhai_wasm/index.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>HeroModels Project Rhai WASM Test</title>
|
||||
<style>
|
||||
body { font-family: sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
|
||||
#output { margin-top: 20px; padding: 10px; border: 1px solid #ccc; background-color: #fff; white-space: pre-wrap; }
|
||||
button { padding: 10px 15px; font-size: 16px; cursor: pointer; background-color: #007bff; color: white; border: none; border-radius: 5px; }
|
||||
button:hover { background-color: #0056b3; }
|
||||
h1 { color: #0056b3; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>HeroModels Project Rhai WASM Test</h1>
|
||||
<p>Open your browser's developer console to see detailed logs from the Rhai script.</p>
|
||||
<button id="runScriptButton">Run Rhai Script in WASM</button>
|
||||
|
||||
<script type="module">
|
||||
// Import the WASM module
|
||||
import init, { run_project_script_wasm } from './pkg/project_rhai_wasm_example.js';
|
||||
|
||||
async function main() {
|
||||
// Initialize the WASM module
|
||||
await init();
|
||||
console.log("WASM module initialized.");
|
||||
|
||||
const runButton = document.getElementById('runScriptButton');
|
||||
runButton.onclick = () => {
|
||||
console.log("Button clicked, attempting to run script...");
|
||||
try {
|
||||
run_project_script_wasm();
|
||||
console.log("run_project_script_wasm called.");
|
||||
} catch (e) {
|
||||
console.error("Error calling run_project_script_wasm:", e);
|
||||
}
|
||||
};
|
||||
// Automatically run the script on load if desired
|
||||
// console.log("Attempting to run script on load...");
|
||||
// try {
|
||||
// run_project_script_wasm();
|
||||
// console.log("run_project_script_wasm called on load.");
|
||||
// } catch (e) {
|
||||
// console.error("Error calling run_project_script_wasm on load:", e);
|
||||
// }
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
126
heromodels/examples/project_rhai_wasm/src/lib.rs
Normal file
126
heromodels/examples/project_rhai_wasm/src/lib.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
use heromodels::db::{OurDB, Db}; // Import Db trait
|
||||
use heromodels::models::projects::rhai::register_projects_rhai_module;
|
||||
use rhai::{Engine, Scope, Dynamic, EvalAltResult, Position};
|
||||
use std::sync::Arc;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::console;
|
||||
|
||||
// Called once when the WASM module is instantiated.
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn main_wasm() -> Result<(), JsValue> {
|
||||
// For better panic messages in the browser console
|
||||
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn run_project_script_wasm() -> Result<(), JsValue> {
|
||||
console::log_1(&"Starting Rhai script execution in WASM...".into());
|
||||
|
||||
let script = r#"
|
||||
// Test script for Project Rhai integration
|
||||
|
||||
print("--- Testing Project Rhai Integration (WASM) ---");
|
||||
|
||||
// Create a new project
|
||||
let p1 = new_project()
|
||||
.name("Project Alpha WASM")
|
||||
.description("This is the first test project in WASM.")
|
||||
.owner_id(101)
|
||||
.add_member_id(102)
|
||||
.add_member_id(103)
|
||||
.member_ids([201, 202, 203])
|
||||
.add_tag("important")
|
||||
.add_tag("rhai_test")
|
||||
.add_tag("wasm")
|
||||
.tags(["core", "feature_test", "wasm_run"])
|
||||
.status(Status::InProgress)
|
||||
.priority(Priority::High)
|
||||
.item_type(ItemType::Feature)
|
||||
.add_base_comment(1001);
|
||||
|
||||
print("Created project p1: " + p1);
|
||||
print("p1.name: " + p1.name);
|
||||
print("p1.description: " + p1.description);
|
||||
print("p1.owner_id: " + p1.owner_id);
|
||||
print("p1.member_ids: " + p1.member_ids);
|
||||
print("p1.tags: " + p1.tags);
|
||||
print(`p1.status: ${p1.status.to_string()}`);
|
||||
print(`p1.priority: ${p1.priority.to_string()}`);
|
||||
print(`p1.item_type: ${p1.item_type.to_string()}`);
|
||||
print("p1.id: " + p1.id);
|
||||
print("p1.created_at: " + p1.created_at);
|
||||
print("p1.modified_at: " + p1.modified_at);
|
||||
print("p1.comments: " + p1.comments);
|
||||
|
||||
// Save to DB
|
||||
try {
|
||||
set_project(p1);
|
||||
print("Project p1 saved successfully.");
|
||||
} catch (err) {
|
||||
print("Error saving project p1: " + err);
|
||||
}
|
||||
|
||||
// Retrieve from DB
|
||||
try {
|
||||
let retrieved_p1 = get_project_by_id(1);
|
||||
if retrieved_p1 != () { // Check if Some(project) was returned (None becomes '()')
|
||||
print("Retrieved project by ID 1: " + retrieved_p1);
|
||||
print("Retrieved project name: " + retrieved_p1.name);
|
||||
print("Retrieved project tags: " + retrieved_p1.tags);
|
||||
} else {
|
||||
print("Project with ID 1 not found.");
|
||||
}
|
||||
} catch (err) {
|
||||
print("Error retrieving project by ID 1: " + err);
|
||||
}
|
||||
|
||||
// Test non-existent project
|
||||
try {
|
||||
let non_existent_project = get_project_by_id(999);
|
||||
if non_existent_project != () {
|
||||
print("Error: Found non-existent project 999: " + non_existent_project);
|
||||
} else {
|
||||
print("Correctly did not find project with ID 999.");
|
||||
}
|
||||
} catch (err) {
|
||||
print("Error checking for non-existent project: " + err);
|
||||
}
|
||||
|
||||
print("--- Project Rhai Integration Test Complete (WASM) ---");
|
||||
"#;
|
||||
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Redirect Rhai's print to browser console
|
||||
engine.on_print(|text| {
|
||||
console::log_1(&text.into());
|
||||
});
|
||||
|
||||
// Attempt to initialize OurDB. Sled's behavior in WASM with paths is experimental.
|
||||
// It might work as an in-memory/temporary DB, or it might fail.
|
||||
// Using a specific path and reset=true.
|
||||
let db = match OurDB::new("/project_rhai_wasm_db", true) {
|
||||
Ok(db_instance) => Arc::new(db_instance),
|
||||
Err(e) => {
|
||||
let err_msg = format!("Failed to initialize OurDB for WASM: {}", e);
|
||||
console::error_1(&err_msg.into());
|
||||
return Err(JsValue::from_str(&err_msg));
|
||||
}
|
||||
};
|
||||
|
||||
register_projects_rhai_module(&mut engine, db);
|
||||
|
||||
let mut scope = Scope::new();
|
||||
|
||||
match engine.run_with_scope(&mut scope, script) {
|
||||
Ok(_) => console::log_1(&"Rhai script executed successfully in WASM!".into()),
|
||||
Err(e) => {
|
||||
let err_msg = format!("Rhai script execution failed in WASM: {}\nDetails: {:?}", e, e.to_string());
|
||||
console::error_1(&err_msg.into());
|
||||
return Err(JsValue::from_str(&e.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@@ -1,3 +1,4 @@
|
||||
use heromodels::db::{Collection, Db};
|
||||
use heromodels_core::{BaseModelData, Model};
|
||||
use heromodels_derive::model;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -15,16 +16,36 @@ fn main() {
|
||||
println!("Hero Models - Simple Model Example");
|
||||
println!("==================================");
|
||||
|
||||
// Create a new DB instance, reset before every run
|
||||
let db_path = "/tmp/ourdb_simple_model_example";
|
||||
let db = heromodels::db::hero::OurDB::new(db_path, true).expect("Can create DB");
|
||||
|
||||
// Example usage of the generated implementation
|
||||
println!("SimpleUser DB Prefix: {}", SimpleUser::db_prefix());
|
||||
|
||||
// Create a new user with ID 0 (will be auto-generated when saved)
|
||||
let user = SimpleUser {
|
||||
base_data: BaseModelData::new(1),
|
||||
base_data: BaseModelData::new(),
|
||||
login: "johndoe".to_string(),
|
||||
full_name: "John Doe".to_string(),
|
||||
};
|
||||
|
||||
println!("\nSimpleUser ID: {}", user.get_id());
|
||||
println!("SimpleUser DB Keys: {:?}", user.db_keys());
|
||||
println!("\nBefore saving - SimpleUser ID: {}", user.get_id());
|
||||
println!("Before saving - SimpleUser DB Keys: {:?}", user.db_keys());
|
||||
|
||||
// Save the user to the database
|
||||
let collection = db.collection::<SimpleUser>().expect("can open user collection");
|
||||
let (user_id, saved_user) = collection.set(&user).expect("can save user");
|
||||
|
||||
println!("\nAfter saving - SimpleUser ID: {}", saved_user.get_id());
|
||||
println!("After saving - SimpleUser DB Keys: {:?}", saved_user.db_keys());
|
||||
println!("Returned ID: {}", user_id);
|
||||
|
||||
// Verify that the ID was auto-generated
|
||||
assert_eq!(saved_user.get_id(), user_id);
|
||||
assert_ne!(saved_user.get_id(), 0);
|
||||
|
||||
println!("\nExample finished. DB stored at {}", db_path);
|
||||
println!("To clean up, you can manually delete the directory: {}", db_path);
|
||||
}
|
||||
|
||||
|
0
heromodels/expanded_calendar_example.rs
Normal file
0
heromodels/expanded_calendar_example.rs
Normal file
BIN
heromodels/index/0.db
Normal file
BIN
heromodels/index/0.db
Normal file
Binary file not shown.
1
heromodels/index/lookup/.inc
Normal file
1
heromodels/index/lookup/.inc
Normal file
@@ -0,0 +1 @@
|
||||
74
|
BIN
heromodels/index/lookup/data
Normal file
BIN
heromodels/index/lookup/data
Normal file
Binary file not shown.
81
heromodels/run_all_examples.sh
Executable file
81
heromodels/run_all_examples.sh
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to run all examples in the heromodels crate and check if they executed successfully
|
||||
|
||||
# Set colors for output
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to run an example and check if it executed successfully
|
||||
run_example() {
|
||||
local example=$1
|
||||
echo -e "${YELLOW}Running example: ${example}${NC}"
|
||||
|
||||
# Run the example and capture the exit code
|
||||
cargo run --example $example
|
||||
local exit_code=$?
|
||||
|
||||
# Check if the example executed successfully
|
||||
if [ $exit_code -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ Example ${example} executed successfully${NC}"
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}✗ Example ${example} failed with exit code ${exit_code}${NC}"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Make sure we're in the heromodels directory
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
# Get all examples
|
||||
examples=$(find examples -type f -name "*.rs" -not -path "*/*/main.rs" -not -path "*/*/example.rs" | grep -v "/mod.rs$" | sed 's/examples\///g' | sed 's/\.rs$//g' | sort)
|
||||
|
||||
# Add directory examples
|
||||
dir_examples=$(find examples -type d -depth 1 | sed 's/examples\///g' | sort)
|
||||
|
||||
# Initialize counters
|
||||
total=0
|
||||
passed=0
|
||||
failed=0
|
||||
|
||||
# Run all file examples
|
||||
echo -e "${YELLOW}Running file examples...${NC}"
|
||||
for example in $examples; do
|
||||
((total++))
|
||||
if run_example $example; then
|
||||
((passed++))
|
||||
else
|
||||
((failed++))
|
||||
failed_examples="$failed_examples $example"
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
|
||||
# Run all directory examples
|
||||
echo -e "${YELLOW}Running directory examples...${NC}"
|
||||
for example in $dir_examples; do
|
||||
((total++))
|
||||
if run_example $example; then
|
||||
((passed++))
|
||||
else
|
||||
((failed++))
|
||||
failed_examples="$failed_examples $example"
|
||||
fi
|
||||
echo ""
|
||||
done
|
||||
|
||||
# Print summary
|
||||
echo -e "${YELLOW}Summary:${NC}"
|
||||
echo -e "Total examples: ${total}"
|
||||
echo -e "${GREEN}Passed: ${passed}${NC}"
|
||||
if [ $failed -gt 0 ]; then
|
||||
echo -e "${RED}Failed: ${failed}${NC}"
|
||||
echo -e "${RED}Failed examples:${failed_examples}${NC}"
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN}All examples executed successfully!${NC}"
|
||||
exit 0
|
||||
fi
|
@@ -32,8 +32,16 @@ where
|
||||
/// Get an object from its ID. This does not use an index lookup
|
||||
fn get_by_id(&self, id: u32) -> Result<Option<V>, Error<Self::Error>>;
|
||||
|
||||
/// Store an item in the DB.
|
||||
fn set(&self, value: &V) -> Result<(), Error<Self::Error>>;
|
||||
/// Store an item in the DB and return the assigned ID and the updated model.
|
||||
///
|
||||
/// # Important Notes
|
||||
/// - This method does not modify the original model passed as an argument.
|
||||
/// - For new models (with ID 0), an ID will be auto-generated by OurDB.
|
||||
/// - The returned model will have the correct ID and should be used instead of the original model.
|
||||
/// - The original model should not be used after calling this method, as it may have
|
||||
/// an inconsistent state compared to what's in the database.
|
||||
/// - ID 0 is reserved for new models and should not be used for existing models.
|
||||
fn set(&self, value: &V) -> Result<(u32, V), Error<Self::Error>>;
|
||||
|
||||
/// Delete all items from the db with a given index.
|
||||
fn delete<I, Q>(&self, key: &Q) -> Result<(), Error<Self::Error>>
|
||||
@@ -45,8 +53,11 @@ where
|
||||
/// Delete an object with a given ID
|
||||
fn delete_by_id(&self, id: u32) -> Result<(), Error<Self::Error>>;
|
||||
|
||||
/// Get all objects from the colelction
|
||||
/// Get all objects from the collection
|
||||
fn get_all(&self) -> Result<Vec<V>, Error<Self::Error>>;
|
||||
|
||||
/// Begin a transaction for this collection
|
||||
fn begin_transaction(&self) -> Result<Box<dyn Transaction<Error = Self::Error>>, Error<Self::Error>>;
|
||||
}
|
||||
|
||||
/// Errors returned by the DB implementation
|
||||
@@ -58,6 +69,14 @@ pub enum Error<E> {
|
||||
Decode(bincode::error::DecodeError),
|
||||
/// Error encoding a model for storage
|
||||
Encode(bincode::error::EncodeError),
|
||||
/// Invalid ID used (e.g., using ID 0 for an existing model)
|
||||
InvalidId(String),
|
||||
/// ID mismatch (e.g., expected ID 5 but got ID 6)
|
||||
IdMismatch(String),
|
||||
/// ID collision (e.g., trying to create a model with an ID that already exists)
|
||||
IdCollision(String),
|
||||
/// Type error (e.g., trying to get a model of the wrong type)
|
||||
TypeError,
|
||||
}
|
||||
|
||||
impl<E> From<bincode::error::DecodeError> for Error<E> {
|
||||
@@ -71,3 +90,34 @@ impl<E> From<bincode::error::EncodeError> for Error<E> {
|
||||
Error::Encode(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: std::fmt::Debug + std::fmt::Display> std::fmt::Display for Error<E> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::DB(e) => write!(f, "Database error: {}", e),
|
||||
Error::Decode(e) => write!(f, "Failed to decode model: {}", e),
|
||||
Error::Encode(e) => write!(f, "Failed to encode model: {}", e),
|
||||
Error::InvalidId(s) => write!(f, "Invalid ID: {}", s),
|
||||
Error::IdMismatch(s) => write!(f, "ID Mismatch: {}", s),
|
||||
Error::IdCollision(s) => write!(f, "ID Collision: {}", s),
|
||||
Error::TypeError => write!(f, "Type error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
/// A transaction that can be committed or rolled back
|
||||
pub trait Transaction {
|
||||
/// Error type for transaction operations
|
||||
type Error: std::fmt::Debug;
|
||||
|
||||
/// Begin a transaction
|
||||
fn begin(&self) -> Result<(), Error<Self::Error>>;
|
||||
|
||||
/// Commit a transaction
|
||||
fn commit(&self) -> Result<(), Error<Self::Error>>;
|
||||
|
||||
/// Roll back a transaction
|
||||
fn rollback(&self) -> Result<(), Error<Self::Error>>;
|
||||
|
||||
/// Check if a transaction is active
|
||||
fn is_active(&self) -> bool;
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
use crate::db::Transaction;
|
||||
use heromodels_core::{Index, Model};
|
||||
use ourdb::OurDBSetArgs;
|
||||
use serde::Deserialize;
|
||||
@@ -6,36 +7,123 @@ use std::{
|
||||
borrow::Borrow,
|
||||
collections::HashSet,
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicU32, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
/// Configuration for custom ID sequences
|
||||
pub struct IdSequence {
|
||||
/// The starting ID for the sequence
|
||||
start: u32,
|
||||
/// The increment for the sequence
|
||||
increment: u32,
|
||||
/// The current ID in the sequence
|
||||
current: AtomicU32,
|
||||
}
|
||||
|
||||
// Implement Clone manually since AtomicU32 doesn't implement Clone
|
||||
impl Clone for IdSequence {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
start: self.start,
|
||||
increment: self.increment,
|
||||
current: AtomicU32::new(self.current.load(Ordering::SeqCst)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IdSequence {
|
||||
/// Create a new ID sequence with default values (start=1, increment=1)
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
start: 1,
|
||||
increment: 1,
|
||||
current: AtomicU32::new(1),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new ID sequence with custom values
|
||||
pub fn with_config(start: u32, increment: u32) -> Self {
|
||||
Self {
|
||||
start,
|
||||
increment,
|
||||
current: AtomicU32::new(start),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the next ID in the sequence
|
||||
pub fn next_id(&self) -> u32 {
|
||||
self.current.fetch_add(self.increment, Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Reset the sequence to its starting value
|
||||
pub fn reset(&self) {
|
||||
self.current.store(self.start, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Set the current ID in the sequence
|
||||
pub fn set_current(&self, id: u32) {
|
||||
self.current.store(id, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
const BINCODE_CONFIG: bincode::config::Configuration = bincode::config::standard();
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OurDB {
|
||||
index: Arc<Mutex<tst::TST>>,
|
||||
data: Arc<Mutex<ourdb::OurDB>>,
|
||||
// Mutex for ID generation to prevent race conditions
|
||||
id_lock: Arc<Mutex<()>>,
|
||||
// Custom ID sequence configuration
|
||||
id_sequence: Arc<IdSequence>,
|
||||
}
|
||||
|
||||
impl OurDB {
|
||||
/// Create a new instance of ourdb
|
||||
/// Create a new instance of ourdb with default ID sequence (start=1, increment=1)
|
||||
pub fn new(path: impl Into<PathBuf>, reset: bool) -> Result<Self, tst::Error> {
|
||||
Self::with_id_sequence(path, reset, IdSequence::new())
|
||||
}
|
||||
|
||||
/// Create a new instance of ourdb with a custom ID sequence
|
||||
pub fn with_id_sequence(
|
||||
path: impl Into<PathBuf>,
|
||||
reset: bool,
|
||||
id_sequence: IdSequence,
|
||||
) -> Result<Self, tst::Error> {
|
||||
let mut base_path = path.into();
|
||||
let mut data_path = base_path.clone();
|
||||
base_path.push("index");
|
||||
data_path.push("data");
|
||||
|
||||
let data_db = ourdb::OurDB::new(ourdb::OurDBConfig {
|
||||
incremental_mode: false,
|
||||
let mut data_db = ourdb::OurDB::new(ourdb::OurDBConfig {
|
||||
incremental_mode: true,
|
||||
path: data_path,
|
||||
file_size: None,
|
||||
keysize: None,
|
||||
reset: Some(reset),
|
||||
})?;
|
||||
let index_db = tst::TST::new(base_path.to_str().expect("Path is valid UTF-8"), reset)?;
|
||||
// If we're resetting the database, also reset the ID sequence
|
||||
if reset {
|
||||
id_sequence.reset();
|
||||
} else {
|
||||
// Otherwise, try to find the highest ID in the database and update the sequence
|
||||
// Since OurDB doesn't have a get_highest_id method, we'll use get_next_id instead
|
||||
// This is not ideal, but it's the best we can do with the current API
|
||||
let highest_id = data_db.get_next_id().unwrap_or(id_sequence.start);
|
||||
if highest_id >= id_sequence.start {
|
||||
id_sequence.set_current(highest_id + id_sequence.increment);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(OurDB {
|
||||
index: Arc::new(Mutex::new(index_db)),
|
||||
data: Arc::new(Mutex::new(data_db)),
|
||||
id_lock: Arc::new(Mutex::new(())),
|
||||
id_sequence: Arc::new(id_sequence),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -56,6 +144,20 @@ where
|
||||
{
|
||||
type Error = tst::Error;
|
||||
|
||||
/// Begin a transaction for this collection
|
||||
fn begin_transaction(
|
||||
&self,
|
||||
) -> Result<Box<dyn super::Transaction<Error = Self::Error>>, super::Error<Self::Error>> {
|
||||
// Create a new transaction
|
||||
let transaction = OurDBTransaction::new();
|
||||
|
||||
// Begin the transaction
|
||||
transaction.begin()?;
|
||||
|
||||
// Return the transaction
|
||||
Ok(Box::new(transaction))
|
||||
}
|
||||
|
||||
fn get<I, Q>(&self, key: &Q) -> Result<Vec<M>, super::Error<Self::Error>>
|
||||
where
|
||||
I: Index<Model = M>,
|
||||
@@ -87,73 +189,168 @@ where
|
||||
Self::get_ourdb_value(&mut data_db, id)
|
||||
}
|
||||
|
||||
fn set(&self, value: &M) -> Result<(), super::Error<Self::Error>> {
|
||||
// Before inserting the new object, check if an object with this ID already exists. If it does, we potentially need to update indices.
|
||||
let mut data_db = self.data.lock().expect("can lock data DB");
|
||||
let old_obj: Option<M> = Self::get_ourdb_value(&mut data_db, value.get_id())?;
|
||||
let (indices_to_delete, indices_to_add) = if let Some(old_obj) = old_obj {
|
||||
let mut indices_to_delete = vec![];
|
||||
let mut indices_to_add = vec![];
|
||||
let old_indices = old_obj.db_keys();
|
||||
let new_indices = value.db_keys();
|
||||
for old_index in old_indices {
|
||||
for new_index in &new_indices {
|
||||
if old_index.name == new_index.name {
|
||||
if old_index.value != new_index.value {
|
||||
// different value now, remove index
|
||||
indices_to_delete.push(old_index);
|
||||
// and later add the new one
|
||||
indices_to_add.push(new_index.clone());
|
||||
break;
|
||||
fn set(&self, value: &M) -> Result<(u32, M), super::Error<Self::Error>> {
|
||||
// For now, we'll skip using transactions to avoid type inference issues
|
||||
// In a real implementation, you would use a proper transaction mechanism
|
||||
|
||||
// Use a result variable to track success/failure
|
||||
let result = (|| {
|
||||
// Before inserting the new object, check if an object with this ID already exists. If it does, we potentially need to update indices.
|
||||
let mut data_db = self.data.lock().expect("can lock data DB");
|
||||
let old_obj: Option<M> = Self::get_ourdb_value(&mut data_db, value.get_id())?;
|
||||
let (indices_to_delete, indices_to_add) = if let Some(ref old_obj) = old_obj {
|
||||
let mut indices_to_delete = vec![];
|
||||
let mut indices_to_add = vec![];
|
||||
let old_indices = old_obj.db_keys();
|
||||
let new_indices = value.db_keys();
|
||||
for old_index in old_indices {
|
||||
for new_index in &new_indices {
|
||||
if old_index.name == new_index.name {
|
||||
if old_index.value != new_index.value {
|
||||
// different value now, remove index
|
||||
indices_to_delete.push(old_index);
|
||||
// and later add the new one
|
||||
indices_to_add.push(new_index.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// NOTE: we assume here that the index keys are stable, i.e. new index fields don't appear
|
||||
// and existing ones don't dissapear
|
||||
(indices_to_delete, indices_to_add)
|
||||
} else {
|
||||
(vec![], value.db_keys())
|
||||
};
|
||||
|
||||
let mut index_db = self.index.lock().expect("can lock index db");
|
||||
// First delete old indices which need to change
|
||||
for old_index in indices_to_delete {
|
||||
let key = Self::index_key(M::db_prefix(), old_index.name, &old_index.value);
|
||||
let raw_ids = index_db.get(&key)?;
|
||||
let (mut ids, _): (HashSet<u32>, _) =
|
||||
bincode::serde::decode_from_slice(&raw_ids, BINCODE_CONFIG)?;
|
||||
ids.remove(&value.get_id());
|
||||
if ids.is_empty() {
|
||||
// This was the last ID with this index value, remove index entirely
|
||||
index_db.delete(&key)?;
|
||||
// NOTE: we assume here that the index keys are stable, i.e. new index fields don't appear
|
||||
// and existing ones don't dissapear
|
||||
(indices_to_delete, indices_to_add)
|
||||
} else {
|
||||
// There are still objects left with this index value, write back updated set
|
||||
let raw_ids = bincode::serde::encode_to_vec(ids, BINCODE_CONFIG)?;
|
||||
index_db.set(&key, raw_ids)?;
|
||||
(vec![], value.db_keys())
|
||||
};
|
||||
|
||||
let mut index_db = self.index.lock().expect("can lock index db");
|
||||
// First delete old indices which need to change
|
||||
for old_index in indices_to_delete {
|
||||
let key = Self::index_key(M::db_prefix(), old_index.name, &old_index.value);
|
||||
let raw_ids = index_db.get(&key)?;
|
||||
let (mut ids, _): (HashSet<u32>, _) =
|
||||
bincode::serde::decode_from_slice(&raw_ids, BINCODE_CONFIG)?;
|
||||
ids.remove(&value.get_id());
|
||||
if ids.is_empty() {
|
||||
// This was the last ID with this index value, remove index entirely
|
||||
index_db.delete(&key)?;
|
||||
} else {
|
||||
// There are still objects left with this index value, write back updated set
|
||||
let raw_ids = bincode::serde::encode_to_vec(ids, BINCODE_CONFIG)?;
|
||||
index_db.set(&key, raw_ids)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set or update the object
|
||||
let v = bincode::serde::encode_to_vec(value, BINCODE_CONFIG)?;
|
||||
let id = value.get_id();
|
||||
data_db.set(OurDBSetArgs {
|
||||
id: Some(id),
|
||||
data: &v,
|
||||
})?;
|
||||
// Get the current ID
|
||||
let id = value.get_id();
|
||||
|
||||
// Now add the new indices
|
||||
for index_key in indices_to_add {
|
||||
let key = Self::index_key(M::db_prefix(), index_key.name, &index_key.value);
|
||||
// Load the existing id set for the index or create a new set
|
||||
let mut existing_ids =
|
||||
Self::get_tst_value::<HashSet<u32>>(&mut index_db, &key)?.unwrap_or_default();
|
||||
existing_ids.insert(id);
|
||||
let encoded_ids = bincode::serde::encode_to_vec(existing_ids, BINCODE_CONFIG)?;
|
||||
index_db.set(&key, encoded_ids)?;
|
||||
}
|
||||
// Validate that ID 0 is only used for new models
|
||||
if id == 0 {
|
||||
// Check if this model already exists in the database
|
||||
// If it does, it's an error to use ID 0 for an existing model
|
||||
if let Some(existing) = Self::get_ourdb_value::<M>(&mut data_db, id)? {
|
||||
return Err(super::Error::InvalidId(format!(
|
||||
"ID 0 is reserved for new models. Found existing model with ID 0: {:?}",
|
||||
existing
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
// Validate that IDs > 0 are only used for existing models
|
||||
// If the model doesn't exist, it's an error to use a specific ID
|
||||
if id > 0 && Self::get_ourdb_value::<M>(&mut data_db, id)?.is_none() {
|
||||
return Err(super::Error::InvalidId(format!(
|
||||
"ID {} does not exist in the database. Use ID 0 for new models.",
|
||||
id
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
// Check for ID collisions when manually setting an ID
|
||||
if id > 0 && Self::get_ourdb_value::<M>(&mut data_db, id)?.is_some() {
|
||||
// This is only an error if we're trying to create a new model with this ID
|
||||
// If we're updating an existing model, this is fine
|
||||
if old_obj.is_none() {
|
||||
return Err(super::Error::IdCollision(format!(
|
||||
"ID {} already exists in the database",
|
||||
id
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If id is 0, it's a new object, so let OurDB auto-generate an ID
|
||||
// Otherwise, it's an update to an existing object
|
||||
let id_param = if id == 0 { None } else { Some(id) };
|
||||
|
||||
// Thread-safe approach for handling ID assignment
|
||||
let assigned_id = if id == 0 {
|
||||
// For new objects, serialize with ID 0
|
||||
let v = bincode::serde::encode_to_vec(value, BINCODE_CONFIG)?;
|
||||
|
||||
// Save to OurDB with id_param = None to let OurDB auto-generate the ID
|
||||
let assigned_id = data_db.set(OurDBSetArgs {
|
||||
id: id_param,
|
||||
data: &v,
|
||||
})?;
|
||||
|
||||
// Now that we have the actual assigned ID, create a new model with the correct ID
|
||||
// and save it again to ensure the serialized data contains the correct ID
|
||||
let mut value_clone = value.clone();
|
||||
let base_data = value_clone.base_data_mut();
|
||||
base_data.id = assigned_id;
|
||||
|
||||
// Serialize the updated model
|
||||
let v = bincode::serde::encode_to_vec(&value_clone, BINCODE_CONFIG)?;
|
||||
|
||||
// Save again with the explicit ID
|
||||
data_db.set(OurDBSetArgs {
|
||||
id: Some(assigned_id),
|
||||
data: &v,
|
||||
})?;
|
||||
|
||||
// Return the assigned ID
|
||||
assigned_id
|
||||
} else {
|
||||
// For existing objects, just serialize and save
|
||||
let v = bincode::serde::encode_to_vec(value, BINCODE_CONFIG)?;
|
||||
|
||||
// Save to OurDB with the existing ID
|
||||
let assigned_id = data_db.set(OurDBSetArgs {
|
||||
id: id_param,
|
||||
data: &v,
|
||||
})?;
|
||||
|
||||
// Return the existing ID
|
||||
assigned_id
|
||||
};
|
||||
|
||||
// Now add the new indices
|
||||
for index_key in indices_to_add {
|
||||
let key = Self::index_key(M::db_prefix(), index_key.name, &index_key.value);
|
||||
// Load the existing id set for the index or create a new set
|
||||
let mut existing_ids =
|
||||
Self::get_tst_value::<HashSet<u32>>(&mut index_db, &key)?.unwrap_or_default();
|
||||
// Use the assigned ID for new objects
|
||||
existing_ids.insert(assigned_id);
|
||||
let encoded_ids = bincode::serde::encode_to_vec(existing_ids, BINCODE_CONFIG)?;
|
||||
index_db.set(&key, encoded_ids)?;
|
||||
}
|
||||
|
||||
// Get the updated model from the database
|
||||
let updated_model =
|
||||
Self::get_ourdb_value::<M>(&mut data_db, assigned_id)?.ok_or_else(|| {
|
||||
super::Error::InvalidId(format!(
|
||||
"Failed to retrieve model with ID {} after saving",
|
||||
assigned_id
|
||||
))
|
||||
})?;
|
||||
|
||||
// Return the assigned ID and the updated model
|
||||
Ok((assigned_id, updated_model))
|
||||
})();
|
||||
|
||||
// Return the result directly
|
||||
// In a real implementation, you would commit or rollback the transaction here
|
||||
result
|
||||
}
|
||||
|
||||
fn delete<I, Q>(&self, key: &Q) -> Result<(), super::Error<Self::Error>>
|
||||
@@ -228,7 +425,64 @@ where
|
||||
}
|
||||
|
||||
fn get_all(&self) -> Result<Vec<M>, super::Error<Self::Error>> {
|
||||
todo!("OurDB doesn't have a list all method yet")
|
||||
let mut index_db = self.index.lock().expect("can lock index DB");
|
||||
let mut data_db = self.data.lock().expect("can lock data DB");
|
||||
|
||||
let prefix = M::db_prefix();
|
||||
let mut all_object_ids: HashSet<u32> = HashSet::new();
|
||||
|
||||
// Use getall to find all index entries (values are serialized HashSet<u32>) for the given model prefix.
|
||||
match index_db.getall(prefix) {
|
||||
Ok(list_of_raw_ids_set_bytes) => {
|
||||
for raw_ids_set_bytes in list_of_raw_ids_set_bytes {
|
||||
// Each item in the list is a bincode-serialized HashSet<u32> of object IDs.
|
||||
match bincode::serde::decode_from_slice::<HashSet<u32>, _>(&raw_ids_set_bytes, BINCODE_CONFIG) {
|
||||
Ok((ids_set, _)) => { // Destructure the tuple (HashSet<u32>, usize)
|
||||
all_object_ids.extend(ids_set);
|
||||
}
|
||||
Err(e) => {
|
||||
// If deserialization of an ID set fails, propagate as a decode error.
|
||||
return Err(super::Error::Decode(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(tst::Error::PrefixNotFound(_)) => {
|
||||
// No index entries found for this prefix, meaning no objects of this type exist.
|
||||
// Note: tst::getall might return Ok(vec![]) in this case instead of PrefixNotFound.
|
||||
// Depending on tst implementation, this arm might be redundant if getall returns empty vec.
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
Err(e) => {
|
||||
// Other TST errors.
|
||||
return Err(super::Error::DB(e));
|
||||
}
|
||||
}
|
||||
|
||||
let mut results: Vec<M> = Vec::with_capacity(all_object_ids.len());
|
||||
for obj_id in all_object_ids {
|
||||
match Self::get_ourdb_value::<M>(&mut data_db, obj_id) {
|
||||
Ok(Some(obj)) => {
|
||||
results.push(obj);
|
||||
}
|
||||
Ok(None) => {
|
||||
// This case implies an inconsistency: an object ID was in an index,
|
||||
// but the object itself was not found in the data store.
|
||||
// Log this an issue, but continue processing other valid objects.
|
||||
// Consider how strictly this should be handled (e.g., return error or log and skip).
|
||||
eprintln!(
|
||||
"[heromodels] Warning: Object ID {} found in index for model '{}' but not in data store.",
|
||||
obj_id,
|
||||
M::db_prefix()
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
// If fetching or decoding a specific object fails.
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,6 +492,29 @@ impl OurDB {
|
||||
format!("{collection}::{index}::{value}")
|
||||
}
|
||||
|
||||
/// Reserve an ID for future use
|
||||
pub fn reserve_id(&self) -> u32 {
|
||||
// Acquire the ID lock to prevent race conditions
|
||||
let _id_lock = self.id_lock.lock().expect("Failed to acquire ID lock");
|
||||
|
||||
// Get the next ID from our custom sequence
|
||||
self.id_sequence.next_id()
|
||||
}
|
||||
|
||||
/// Reserve multiple IDs for future use
|
||||
pub fn reserve_ids(&self, count: u32) -> Vec<u32> {
|
||||
// Acquire the ID lock to prevent race conditions
|
||||
let _id_lock = self.id_lock.lock().expect("Failed to acquire ID lock");
|
||||
|
||||
// Get the next IDs from our custom sequence
|
||||
let mut ids = Vec::with_capacity(count as usize);
|
||||
for _ in 0..count {
|
||||
ids.push(self.id_sequence.next_id());
|
||||
}
|
||||
|
||||
ids
|
||||
}
|
||||
|
||||
/// Wrapper to load values from ourdb and transform a not found error in to Ok(None)
|
||||
fn get_ourdb_value<V>(
|
||||
data: &mut ourdb::OurDB,
|
||||
@@ -285,3 +562,86 @@ impl From<ourdb::Error> for super::Error<tst::Error> {
|
||||
super::Error::DB(tst::Error::OurDB(value))
|
||||
}
|
||||
}
|
||||
|
||||
/// A transaction for OurDB
|
||||
///
|
||||
/// Note: This is a simplified implementation that doesn't actually provide
|
||||
/// ACID guarantees. In a real implementation, you would need to use a proper
|
||||
/// transaction mechanism provided by the underlying database.
|
||||
///
|
||||
/// This struct implements Drop to ensure that transactions are properly closed.
|
||||
/// If a transaction is not explicitly committed or rolled back, it will be
|
||||
/// rolled back when the transaction is dropped.
|
||||
struct OurDBTransaction {
|
||||
active: std::sync::atomic::AtomicBool,
|
||||
}
|
||||
|
||||
impl OurDBTransaction {
|
||||
/// Create a new transaction
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
active: std::sync::atomic::AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for OurDBTransaction {
|
||||
fn drop(&mut self) {
|
||||
// If the transaction is still active when dropped, roll it back
|
||||
if self.active.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
// We can't return an error from drop, so we just log it
|
||||
eprintln!(
|
||||
"Warning: Transaction was dropped without being committed or rolled back. Rolling back automatically."
|
||||
);
|
||||
self.active
|
||||
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Transaction for OurDBTransaction {
|
||||
type Error = tst::Error;
|
||||
|
||||
/// Begin the transaction
|
||||
fn begin(&self) -> Result<(), super::Error<Self::Error>> {
|
||||
// In a real implementation, you would start a transaction in the underlying database
|
||||
// For now, we just set the active flag
|
||||
self.active.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Commit the transaction
|
||||
fn commit(&self) -> Result<(), super::Error<Self::Error>> {
|
||||
// In a real implementation, you would commit the transaction in the underlying database
|
||||
// For now, we just check if the transaction is active
|
||||
if !self.active.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
return Err(super::Error::InvalidId(
|
||||
"Cannot commit an inactive transaction".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
self.active
|
||||
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Roll back the transaction
|
||||
fn rollback(&self) -> Result<(), super::Error<Self::Error>> {
|
||||
// In a real implementation, you would roll back the transaction in the underlying database
|
||||
// For now, we just check if the transaction is active
|
||||
if !self.active.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
return Err(super::Error::InvalidId(
|
||||
"Cannot roll back an inactive transaction".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
self.active
|
||||
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if the transaction is active
|
||||
fn is_active(&self) -> bool {
|
||||
self.active.load(std::sync::atomic::Ordering::SeqCst)
|
||||
}
|
||||
}
|
||||
|
178
heromodels/src/models/biz/company.rs
Normal file
178
heromodels/src/models/biz/company.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use heromodels_core::{BaseModelData, Model, IndexKey, IndexKeyBuilder, Index};
|
||||
use rhai::{CustomType, TypeBuilder}; // For #[derive(CustomType)]
|
||||
|
||||
// --- Enums ---
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum CompanyStatus {
|
||||
Active,
|
||||
Inactive,
|
||||
Suspended,
|
||||
}
|
||||
|
||||
impl Default for CompanyStatus {
|
||||
fn default() -> Self {
|
||||
CompanyStatus::Inactive
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum BusinessType {
|
||||
Coop,
|
||||
Single,
|
||||
Twin,
|
||||
Starter,
|
||||
Global,
|
||||
}
|
||||
|
||||
impl Default for BusinessType {
|
||||
fn default() -> Self {
|
||||
BusinessType::Single
|
||||
}
|
||||
}
|
||||
|
||||
// --- Company Struct ---
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, CustomType)] // Added CustomType
|
||||
pub struct Company {
|
||||
pub base_data: BaseModelData,
|
||||
pub name: String,
|
||||
pub registration_number: String,
|
||||
pub incorporation_date: i64, // Changed to i64 // Timestamp
|
||||
pub fiscal_year_end: String, // e.g., "MM-DD"
|
||||
pub email: String,
|
||||
pub phone: String,
|
||||
pub website: String,
|
||||
pub address: String,
|
||||
pub business_type: BusinessType,
|
||||
pub industry: String,
|
||||
pub description: String,
|
||||
pub status: CompanyStatus,
|
||||
}
|
||||
|
||||
// --- Model Trait Implementation ---
|
||||
|
||||
impl Model for Company {
|
||||
fn db_prefix() -> &'static str {
|
||||
"company"
|
||||
}
|
||||
|
||||
fn get_id(&self) -> u32 {
|
||||
self.base_data.id
|
||||
}
|
||||
|
||||
fn base_data_mut(&mut self) -> &mut BaseModelData {
|
||||
&mut self.base_data
|
||||
}
|
||||
|
||||
// Override db_keys to provide custom indexes if needed
|
||||
fn db_keys(&self) -> Vec<IndexKey> {
|
||||
vec![
|
||||
IndexKeyBuilder::new("name").value(self.name.clone()).build(),
|
||||
IndexKeyBuilder::new("registration_number").value(self.registration_number.clone()).build(),
|
||||
// Add other relevant keys, e.g., by status or type if frequently queried
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
// --- Index Implementations (Example) ---
|
||||
|
||||
pub struct CompanyNameIndex;
|
||||
impl Index for CompanyNameIndex {
|
||||
type Model = Company;
|
||||
type Key = str;
|
||||
fn key() -> &'static str {
|
||||
"name"
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CompanyRegistrationNumberIndex;
|
||||
impl Index for CompanyRegistrationNumberIndex {
|
||||
type Model = Company;
|
||||
type Key = str;
|
||||
fn key() -> &'static str {
|
||||
"registration_number"
|
||||
}
|
||||
}
|
||||
|
||||
// --- Builder Pattern ---
|
||||
|
||||
impl Company {
|
||||
pub fn new(name: String, registration_number: String, incorporation_date: i64) -> Self { // incorporation_date to i64
|
||||
Self {
|
||||
base_data: BaseModelData::new(),
|
||||
name,
|
||||
registration_number,
|
||||
incorporation_date, // This is i64 now
|
||||
fiscal_year_end: String::new(),
|
||||
email: String::new(),
|
||||
phone: String::new(),
|
||||
website: String::new(),
|
||||
address: String::new(),
|
||||
business_type: BusinessType::default(),
|
||||
industry: String::new(),
|
||||
description: String::new(),
|
||||
status: CompanyStatus::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fiscal_year_end(mut self, fiscal_year_end: String) -> Self {
|
||||
self.fiscal_year_end = fiscal_year_end;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn email(mut self, email: String) -> Self {
|
||||
self.email = email;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn phone(mut self, phone: String) -> Self {
|
||||
self.phone = phone;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn website(mut self, website: String) -> Self {
|
||||
self.website = website;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn address(mut self, address: String) -> Self {
|
||||
self.address = address;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn business_type(mut self, business_type: BusinessType) -> Self {
|
||||
self.business_type = business_type;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn industry(mut self, industry: String) -> Self {
|
||||
self.industry = industry;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn description(mut self, description: String) -> Self {
|
||||
self.description = description;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn status(mut self, status: CompanyStatus) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
// Setter for base_data fields if needed directly, though usually handled by Model trait or new()
|
||||
// Builder methods for created_at and modified_at can be more specific if needed,
|
||||
// but Model::build() updates modified_at, and BaseModelData::new() sets created_at.
|
||||
// If direct setting is required for tests or specific scenarios:
|
||||
pub fn set_base_created_at(mut self, created_at: i64) -> Self {
|
||||
self.base_data.created_at = created_at;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_base_modified_at(mut self, modified_at: i64) -> Self {
|
||||
self.base_data.modified_at = modified_at;
|
||||
self
|
||||
}
|
||||
}
|
25
heromodels/src/models/biz/mod.rs
Normal file
25
heromodels/src/models/biz/mod.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
// Business models module
|
||||
// Sub-modules will be declared here
|
||||
|
||||
pub mod company;
|
||||
pub mod product;
|
||||
// pub mod sale;
|
||||
// pub mod shareholder;
|
||||
// pub mod user;
|
||||
|
||||
// Re-export main types from sub-modules
|
||||
pub use company::{Company, CompanyStatus, BusinessType};
|
||||
pub mod shareholder;
|
||||
pub use shareholder::{Shareholder, ShareholderType};
|
||||
pub use product::{Product, ProductType, ProductStatus, ProductComponent};
|
||||
|
||||
pub mod sale;
|
||||
pub use sale::{Sale, SaleItem, SaleStatus};
|
||||
|
||||
// pub use user::{User}; // Assuming a simple User model for now
|
||||
|
||||
|
||||
#[cfg(feature = "rhai")]
|
||||
pub mod rhai;
|
||||
#[cfg(feature = "rhai")]
|
||||
pub use rhai::register_biz_rhai_module;
|
179
heromodels/src/models/biz/product.rs
Normal file
179
heromodels/src/models/biz/product.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
use serde::{Serialize, Deserialize};
|
||||
use heromodels_core::BaseModelData;
|
||||
use heromodels_core::Model;
|
||||
|
||||
// ProductType represents the type of a product
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||
pub enum ProductType {
|
||||
#[default]
|
||||
Product,
|
||||
Service,
|
||||
}
|
||||
|
||||
// ProductStatus represents the status of a product
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||
pub enum ProductStatus {
|
||||
#[default]
|
||||
Available,
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
// ProductComponent represents a component or sub-part of a product.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
|
||||
pub struct ProductComponent {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub quantity: u32,
|
||||
}
|
||||
|
||||
impl ProductComponent {
|
||||
// Minimal constructor
|
||||
pub fn new(name: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
description: String::new(),
|
||||
quantity: 1, // Default quantity to 1
|
||||
}
|
||||
}
|
||||
|
||||
// Builder methods
|
||||
pub fn description(mut self, description: String) -> Self {
|
||||
self.description = description;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn quantity(mut self, quantity: u32) -> Self {
|
||||
self.quantity = quantity;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn name(mut self, name: String) -> Self {
|
||||
self.name = name;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// Product represents a product or service offered
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct Product {
|
||||
pub base_data: BaseModelData,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub price: f64, // Representing currency.Currency for now
|
||||
pub type_: ProductType,
|
||||
pub category: String,
|
||||
pub status: ProductStatus,
|
||||
pub max_amount: u16,
|
||||
pub purchase_till: i64, // Representing ourtime.OurTime
|
||||
pub active_till: i64, // Representing ourtime.OurTime
|
||||
pub components: Vec<ProductComponent>,
|
||||
}
|
||||
|
||||
impl Model for Product {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.base_data.id
|
||||
}
|
||||
|
||||
fn db_prefix() -> &'static str {
|
||||
"prod"
|
||||
}
|
||||
|
||||
fn base_data_mut(&mut self) -> &mut BaseModelData {
|
||||
&mut self.base_data
|
||||
}
|
||||
}
|
||||
|
||||
impl Product {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
base_data: BaseModelData::new(),
|
||||
name: String::new(),
|
||||
description: String::new(),
|
||||
price: 0.0,
|
||||
type_: ProductType::default(),
|
||||
category: String::new(),
|
||||
status: ProductStatus::default(),
|
||||
max_amount: 0,
|
||||
purchase_till: 0,
|
||||
active_till: 0,
|
||||
components: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// Builder methods
|
||||
pub fn name(mut self, name: String) -> Self {
|
||||
self.name = name;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn description(mut self, description: String) -> Self {
|
||||
self.description = description;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn price(mut self, price: f64) -> Self {
|
||||
self.price = price;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn type_(mut self, type_: ProductType) -> Self {
|
||||
self.type_ = type_;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn category(mut self, category: String) -> Self {
|
||||
self.category = category;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn status(mut self, status: ProductStatus) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn max_amount(mut self, max_amount: u16) -> Self {
|
||||
self.max_amount = max_amount;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn purchase_till(mut self, purchase_till: i64) -> Self {
|
||||
self.purchase_till = purchase_till;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn active_till(mut self, active_till: i64) -> Self {
|
||||
self.active_till = active_till;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_component(mut self, component: ProductComponent) -> Self {
|
||||
self.components.push(component);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn components(mut self, components: Vec<ProductComponent>) -> Self {
|
||||
self.components = components;
|
||||
self
|
||||
}
|
||||
|
||||
// BaseModelData field setters
|
||||
pub fn set_base_created_at(mut self, time: i64) -> Self {
|
||||
self.base_data.created_at = time;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_base_modified_at(mut self, time: i64) -> Self {
|
||||
self.base_data.modified_at = time;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_base_comment_id(mut self, comment_id: u32) -> Self {
|
||||
self.base_data.comments.push(comment_id);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_base_comment_ids(mut self, comment_ids: Vec<u32>) -> Self {
|
||||
self.base_data.comments = comment_ids;
|
||||
self
|
||||
}
|
||||
}
|
332
heromodels/src/models/biz/rhai.rs
Normal file
332
heromodels/src/models/biz/rhai.rs
Normal file
@@ -0,0 +1,332 @@
|
||||
use rhai::{Engine, Module, Dynamic, EvalAltResult, Position};
|
||||
use std::sync::Arc;
|
||||
use crate::db::Collection; // For db.set and db.get_by_id
|
||||
use crate::db::hero::OurDB;
|
||||
use super::company::{Company, CompanyStatus, BusinessType};
|
||||
use crate::models::biz::shareholder::{Shareholder, ShareholderType};
|
||||
use crate::models::biz::product::{Product, ProductType, ProductStatus, ProductComponent};
|
||||
use crate::models::biz::sale::{Sale, SaleItem, SaleStatus};
|
||||
use heromodels_core::Model;
|
||||
|
||||
// Helper function to convert i64 to u32, returning a Rhai error if conversion fails
|
||||
fn id_from_i64(id_val: i64) -> Result<u32, Box<EvalAltResult>> {
|
||||
u32::try_from(id_val).map_err(|_| {
|
||||
Box::new(EvalAltResult::ErrorArithmetic(
|
||||
format!("Failed to convert i64 '{}' to u32 for ID", id_val),
|
||||
Position::NONE,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn register_biz_rhai_module(engine: &mut Engine, db: Arc<OurDB>) {
|
||||
let module = Module::new();
|
||||
|
||||
// --- Enum Constants: CompanyStatus ---
|
||||
let mut status_constants_module = Module::new();
|
||||
status_constants_module.set_var("Active", Dynamic::from(CompanyStatus::Active.clone()));
|
||||
status_constants_module.set_var("Inactive", Dynamic::from(CompanyStatus::Inactive.clone()));
|
||||
status_constants_module.set_var("Suspended", Dynamic::from(CompanyStatus::Suspended.clone()));
|
||||
engine.register_static_module("CompanyStatusConstants", status_constants_module.into());
|
||||
engine.register_type_with_name::<CompanyStatus>("CompanyStatus");
|
||||
|
||||
// --- Enum Constants: BusinessType ---
|
||||
let mut business_type_constants_module = Module::new();
|
||||
business_type_constants_module.set_var("Coop", Dynamic::from(BusinessType::Coop.clone()));
|
||||
business_type_constants_module.set_var("Single", Dynamic::from(BusinessType::Single.clone()));
|
||||
business_type_constants_module.set_var("Twin", Dynamic::from(BusinessType::Twin.clone()));
|
||||
business_type_constants_module.set_var("Starter", Dynamic::from(BusinessType::Starter.clone()));
|
||||
business_type_constants_module.set_var("Global", Dynamic::from(BusinessType::Global.clone()));
|
||||
engine.register_static_module("BusinessTypeConstants", business_type_constants_module.into());
|
||||
engine.register_type_with_name::<BusinessType>("BusinessType");
|
||||
|
||||
// --- Company ---
|
||||
engine.register_type_with_name::<Company>("Company");
|
||||
|
||||
// Constructor
|
||||
engine.register_fn("new_company", |name: String, registration_number: String, incorporation_date: i64| -> Result<Company, Box<EvalAltResult>> { Ok(Company::new(name, registration_number, incorporation_date)) });
|
||||
|
||||
// Getters for Company
|
||||
engine.register_get("id", |company: &mut Company| -> Result<i64, Box<EvalAltResult>> { Ok(company.get_id() as i64) });
|
||||
engine.register_get("created_at", |company: &mut Company| -> Result<i64, Box<EvalAltResult>> { Ok(company.base_data.created_at) });
|
||||
engine.register_get("modified_at", |company: &mut Company| -> Result<i64, Box<EvalAltResult>> { Ok(company.base_data.modified_at) });
|
||||
engine.register_get("name", |company: &mut Company| -> Result<String, Box<EvalAltResult>> { Ok(company.name.clone()) });
|
||||
engine.register_get("registration_number", |company: &mut Company| -> Result<String, Box<EvalAltResult>> { Ok(company.registration_number.clone()) });
|
||||
engine.register_get("incorporation_date", |company: &mut Company| -> Result<i64, Box<EvalAltResult>> { Ok(company.incorporation_date as i64) });
|
||||
engine.register_get("fiscal_year_end", |company: &mut Company| -> Result<String, Box<EvalAltResult>> { Ok(company.fiscal_year_end.clone()) });
|
||||
engine.register_get("email", |company: &mut Company| -> Result<String, Box<EvalAltResult>> { Ok(company.email.clone()) });
|
||||
engine.register_get("phone", |company: &mut Company| -> Result<String, Box<EvalAltResult>> { Ok(company.phone.clone()) });
|
||||
engine.register_get("website", |company: &mut Company| -> Result<String, Box<EvalAltResult>> { Ok(company.website.clone()) });
|
||||
engine.register_get("address", |company: &mut Company| -> Result<String, Box<EvalAltResult>> { Ok(company.address.clone()) });
|
||||
engine.register_get("business_type", |company: &mut Company| -> Result<BusinessType, Box<EvalAltResult>> { Ok(company.business_type.clone()) });
|
||||
engine.register_get("industry", |company: &mut Company| -> Result<String, Box<EvalAltResult>> { Ok(company.industry.clone()) });
|
||||
engine.register_get("description", |company: &mut Company| -> Result<String, Box<EvalAltResult>> { Ok(company.description.clone()) });
|
||||
engine.register_get("status", |company: &mut Company| -> Result<CompanyStatus, Box<EvalAltResult>> { Ok(company.status.clone()) });
|
||||
// Builder methods for Company
|
||||
engine.register_fn("fiscal_year_end", |company: Company, fiscal_year_end: String| -> Result<Company, Box<EvalAltResult>> { Ok(company.fiscal_year_end(fiscal_year_end)) });
|
||||
engine.register_fn("email", |company: Company, email: String| -> Result<Company, Box<EvalAltResult>> { Ok(company.email(email)) });
|
||||
engine.register_fn("phone", |company: Company, phone: String| -> Result<Company, Box<EvalAltResult>> { Ok(company.phone(phone)) });
|
||||
engine.register_fn("website", |company: Company, website: String| -> Result<Company, Box<EvalAltResult>> { Ok(company.website(website)) });
|
||||
engine.register_fn("address", |company: Company, address: String| -> Result<Company, Box<EvalAltResult>> { Ok(company.address(address)) });
|
||||
engine.register_fn("business_type", |company: Company, business_type: BusinessType| -> Result<Company, Box<EvalAltResult>> { Ok(company.business_type(business_type)) });
|
||||
engine.register_fn("industry", |company: Company, industry: String| -> Result<Company, Box<EvalAltResult>> { Ok(company.industry(industry)) });
|
||||
engine.register_fn("description", |company: Company, description: String| -> Result<Company, Box<EvalAltResult>> { Ok(company.description(description)) });
|
||||
engine.register_fn("status", |company: Company, status: CompanyStatus| -> Result<Company, Box<EvalAltResult>> { Ok(company.status(status)) });
|
||||
engine.register_fn("set_base_created_at", |company: Company, created_at: i64| -> Result<Company, Box<EvalAltResult>> { Ok(company.set_base_created_at(created_at)) });
|
||||
engine.register_fn("set_base_modified_at", |company: Company, modified_at: i64| -> Result<Company, Box<EvalAltResult>> { Ok(company.set_base_modified_at(modified_at)) });
|
||||
|
||||
// --- Enum Constants: ShareholderType ---
|
||||
let mut shareholder_type_constants_module = Module::new();
|
||||
shareholder_type_constants_module.set_var("Individual", Dynamic::from(ShareholderType::Individual.clone()));
|
||||
shareholder_type_constants_module.set_var("Corporate", Dynamic::from(ShareholderType::Corporate.clone()));
|
||||
engine.register_static_module("ShareholderTypeConstants", shareholder_type_constants_module.into());
|
||||
engine.register_type_with_name::<ShareholderType>("ShareholderType");
|
||||
|
||||
// --- Shareholder ---
|
||||
engine.register_type_with_name::<Shareholder>("Shareholder");
|
||||
|
||||
// Constructor for Shareholder (minimal, takes only ID)
|
||||
engine.register_fn("new_shareholder", || -> Result<Shareholder, Box<EvalAltResult>> { Ok(Shareholder::new()) });
|
||||
|
||||
// Getters for Shareholder
|
||||
engine.register_get("id", |shareholder: &mut Shareholder| -> Result<i64, Box<EvalAltResult>> { Ok(shareholder.get_id() as i64) });
|
||||
engine.register_get("created_at", |shareholder: &mut Shareholder| -> Result<i64, Box<EvalAltResult>> { Ok(shareholder.base_data.created_at) });
|
||||
engine.register_get("modified_at", |shareholder: &mut Shareholder| -> Result<i64, Box<EvalAltResult>> { Ok(shareholder.base_data.modified_at) });
|
||||
engine.register_get("company_id", |shareholder: &mut Shareholder| -> Result<i64, Box<EvalAltResult>> { Ok(shareholder.company_id as i64) });
|
||||
engine.register_get("user_id", |shareholder: &mut Shareholder| -> Result<i64, Box<EvalAltResult>> { Ok(shareholder.user_id as i64) });
|
||||
engine.register_get("name", |shareholder: &mut Shareholder| -> Result<String, Box<EvalAltResult>> { Ok(shareholder.name.clone()) });
|
||||
engine.register_get("shares", |shareholder: &mut Shareholder| -> Result<f64, Box<EvalAltResult>> { Ok(shareholder.shares) });
|
||||
engine.register_get("percentage", |shareholder: &mut Shareholder| -> Result<f64, Box<EvalAltResult>> { Ok(shareholder.percentage) });
|
||||
engine.register_get("type_", |shareholder: &mut Shareholder| -> Result<ShareholderType, Box<EvalAltResult>> { Ok(shareholder.type_.clone()) });
|
||||
engine.register_get("since", |shareholder: &mut Shareholder| -> Result<i64, Box<EvalAltResult>> { Ok(shareholder.since) });
|
||||
|
||||
// Builder methods for Shareholder
|
||||
engine.register_fn("company_id", |shareholder: Shareholder, company_id: i64| -> Result<Shareholder, Box<EvalAltResult>> { Ok(shareholder.company_id(company_id as u32)) });
|
||||
engine.register_fn("user_id", |shareholder: Shareholder, user_id: i64| -> Result<Shareholder, Box<EvalAltResult>> { Ok(shareholder.user_id(user_id as u32)) });
|
||||
engine.register_fn("name", |shareholder: Shareholder, name: String| -> Result<Shareholder, Box<EvalAltResult>> { Ok(shareholder.name(name)) });
|
||||
engine.register_fn("shares", |shareholder: Shareholder, shares: f64| -> Result<Shareholder, Box<EvalAltResult>> { Ok(shareholder.shares(shares)) });
|
||||
engine.register_fn("percentage", |shareholder: Shareholder, percentage: f64| -> Result<Shareholder, Box<EvalAltResult>> { Ok(shareholder.percentage(percentage)) });
|
||||
engine.register_fn("type_", |shareholder: Shareholder, type_: ShareholderType| -> Result<Shareholder, Box<EvalAltResult>> { Ok(shareholder.type_(type_)) });
|
||||
engine.register_fn("since", |shareholder: Shareholder, since: i64| -> Result<Shareholder, Box<EvalAltResult>> { Ok(shareholder.since(since)) });
|
||||
engine.register_fn("set_base_created_at", |shareholder: Shareholder, created_at: i64| -> Result<Shareholder, Box<EvalAltResult>> { Ok(shareholder.set_base_created_at(created_at)) });
|
||||
engine.register_fn("set_base_modified_at", |shareholder: Shareholder, modified_at: i64| -> Result<Shareholder, Box<EvalAltResult>> { Ok(shareholder.set_base_modified_at(modified_at)) });
|
||||
|
||||
|
||||
// --- Enum Constants: ProductType ---
|
||||
let mut product_type_constants_module = Module::new();
|
||||
product_type_constants_module.set_var("Product", Dynamic::from(ProductType::Product.clone()));
|
||||
product_type_constants_module.set_var("Service", Dynamic::from(ProductType::Service.clone()));
|
||||
engine.register_static_module("ProductTypeConstants", product_type_constants_module.into());
|
||||
engine.register_type_with_name::<ProductType>("ProductType");
|
||||
|
||||
// --- Enum Constants: ProductStatus ---
|
||||
let mut product_status_constants_module = Module::new();
|
||||
product_status_constants_module.set_var("Available", Dynamic::from(ProductStatus::Available.clone()));
|
||||
product_status_constants_module.set_var("Unavailable", Dynamic::from(ProductStatus::Unavailable.clone()));
|
||||
engine.register_static_module("ProductStatusConstants", product_status_constants_module.into());
|
||||
engine.register_type_with_name::<ProductStatus>("ProductStatus");
|
||||
|
||||
// --- Enum Constants: SaleStatus ---
|
||||
let mut sale_status_module = Module::new();
|
||||
sale_status_module.set_var("Pending", Dynamic::from(SaleStatus::Pending.clone()));
|
||||
sale_status_module.set_var("Completed", Dynamic::from(SaleStatus::Completed.clone()));
|
||||
sale_status_module.set_var("Cancelled", Dynamic::from(SaleStatus::Cancelled.clone()));
|
||||
engine.register_static_module("SaleStatusConstants", sale_status_module.into());
|
||||
engine.register_type_with_name::<SaleStatus>("SaleStatus");
|
||||
|
||||
// --- ProductComponent ---
|
||||
engine.register_type_with_name::<ProductComponent>("ProductComponent")
|
||||
.register_fn("new_product_component", |name: String| -> Result<ProductComponent, Box<EvalAltResult>> { Ok(ProductComponent::new(name)) })
|
||||
.register_get("name", |pc: &mut ProductComponent| -> Result<String, Box<EvalAltResult>> { Ok(pc.name.clone()) })
|
||||
.register_fn("name", |pc: ProductComponent, name: String| -> Result<ProductComponent, Box<EvalAltResult>> { Ok(pc.name(name)) })
|
||||
.register_get("description", |pc: &mut ProductComponent| -> Result<String, Box<EvalAltResult>> { Ok(pc.description.clone()) })
|
||||
.register_fn("description", |pc: ProductComponent, description: String| -> Result<ProductComponent, Box<EvalAltResult>> { Ok(pc.description(description)) })
|
||||
.register_get("quantity", |pc: &mut ProductComponent| -> Result<i64, Box<EvalAltResult>> { Ok(pc.quantity as i64) })
|
||||
.register_fn("quantity", |pc: ProductComponent, quantity: i64| -> Result<ProductComponent, Box<EvalAltResult>> { Ok(pc.quantity(quantity as u32)) });
|
||||
|
||||
// --- Product ---
|
||||
engine.register_type_with_name::<Product>("Product")
|
||||
.register_fn("new_product", || -> Result<Product, Box<EvalAltResult>> { Ok(Product::new()) })
|
||||
// Getters for Product
|
||||
.register_get("id", |p: &mut Product| -> Result<i64, Box<EvalAltResult>> { Ok(p.base_data.id as i64) })
|
||||
.register_get("name", |p: &mut Product| -> Result<String, Box<EvalAltResult>> { Ok(p.name.clone()) })
|
||||
.register_get("description", |p: &mut Product| -> Result<String, Box<EvalAltResult>> { Ok(p.description.clone()) })
|
||||
.register_get("price", |p: &mut Product| -> Result<f64, Box<EvalAltResult>> { Ok(p.price) })
|
||||
.register_get("type_", |p: &mut Product| -> Result<ProductType, Box<EvalAltResult>> { Ok(p.type_.clone()) })
|
||||
.register_get("category", |p: &mut Product| -> Result<String, Box<EvalAltResult>> { Ok(p.category.clone()) })
|
||||
.register_get("status", |p: &mut Product| -> Result<ProductStatus, Box<EvalAltResult>> { Ok(p.status.clone()) })
|
||||
.register_get("max_amount", |p: &mut Product| -> Result<i64, Box<EvalAltResult>> { Ok(p.max_amount as i64) })
|
||||
.register_get("purchase_till", |p: &mut Product| -> Result<i64, Box<EvalAltResult>> { Ok(p.purchase_till) })
|
||||
.register_get("active_till", |p: &mut Product| -> Result<i64, Box<EvalAltResult>> { Ok(p.active_till) })
|
||||
.register_get("components", |p: &mut Product| -> Result<rhai::Array, Box<EvalAltResult>> {
|
||||
let rhai_array = p.components.iter().cloned().map(Dynamic::from).collect::<rhai::Array>();
|
||||
Ok(rhai_array)
|
||||
})
|
||||
// Getters for BaseModelData fields
|
||||
.register_get("created_at", |p: &mut Product| -> Result<i64, Box<EvalAltResult>> { Ok(p.base_data.created_at) })
|
||||
.register_get("modified_at", |p: &mut Product| -> Result<i64, Box<EvalAltResult>> { Ok(p.base_data.modified_at) })
|
||||
.register_get("comments", |p: &mut Product| -> Result<Vec<i64>, Box<EvalAltResult>> { Ok(p.base_data.comments.iter().map(|&id| id as i64).collect()) })
|
||||
// Builder methods for Product
|
||||
.register_fn("name", |p: Product, name: String| -> Result<Product, Box<EvalAltResult>> { Ok(p.name(name)) })
|
||||
.register_fn("description", |p: Product, description: String| -> Result<Product, Box<EvalAltResult>> { Ok(p.description(description)) })
|
||||
.register_fn("price", |p: Product, price: f64| -> Result<Product, Box<EvalAltResult>> { Ok(p.price(price)) })
|
||||
.register_fn("type_", |p: Product, type_: ProductType| -> Result<Product, Box<EvalAltResult>> { Ok(p.type_(type_)) })
|
||||
.register_fn("category", |p: Product, category: String| -> Result<Product, Box<EvalAltResult>> { Ok(p.category(category)) })
|
||||
.register_fn("status", |p: Product, status: ProductStatus| -> Result<Product, Box<EvalAltResult>> { Ok(p.status(status)) })
|
||||
.register_fn("max_amount", |p: Product, max_amount: i64| -> Result<Product, Box<EvalAltResult>> { Ok(p.max_amount(max_amount as u16)) })
|
||||
.register_fn("purchase_till", |p: Product, purchase_till: i64| -> Result<Product, Box<EvalAltResult>> { Ok(p.purchase_till(purchase_till)) })
|
||||
.register_fn("active_till", |p: Product, active_till: i64| -> Result<Product, Box<EvalAltResult>> { Ok(p.active_till(active_till)) })
|
||||
.register_fn("add_component", |p: Product, component: ProductComponent| -> Result<Product, Box<EvalAltResult>> { Ok(p.add_component(component)) })
|
||||
.register_fn("components", |p: Product, components: Vec<ProductComponent>| -> Result<Product, Box<EvalAltResult>> { Ok(p.components(components)) })
|
||||
.register_fn("set_base_created_at", |p: Product, time: i64| -> Result<Product, Box<EvalAltResult>> { Ok(p.set_base_created_at(time)) })
|
||||
.register_fn("set_base_modified_at", |p: Product, time: i64| -> Result<Product, Box<EvalAltResult>> { Ok(p.set_base_modified_at(time)) })
|
||||
.register_fn("add_base_comment_id", |p: Product, comment_id: i64| -> Result<Product, Box<EvalAltResult>> { Ok(p.add_base_comment_id(id_from_i64(comment_id)?)) })
|
||||
.register_fn("set_base_comment_ids", |p: Product, comment_ids: Vec<i64>| -> Result<Product, Box<EvalAltResult>> {
|
||||
let u32_ids = comment_ids.into_iter().map(id_from_i64).collect::<Result<Vec<u32>, _>>()?;
|
||||
Ok(p.set_base_comment_ids(u32_ids))
|
||||
});
|
||||
|
||||
// --- SaleItem ---
|
||||
engine.register_type_with_name::<SaleItem>("SaleItem");
|
||||
engine.register_fn("new_sale_item", |product_id_i64: i64, name: String, quantity_i64: i64, unit_price: f64, subtotal: f64| -> Result<SaleItem, Box<EvalAltResult>> {
|
||||
Ok(SaleItem::new(id_from_i64(product_id_i64)?, name, quantity_i64 as i32, unit_price, subtotal))
|
||||
});
|
||||
|
||||
// Getters for SaleItem
|
||||
engine.register_get("product_id", |si: &mut SaleItem| -> Result<i64, Box<EvalAltResult>> { Ok(si.product_id as i64) });
|
||||
engine.register_get("name", |si: &mut SaleItem| -> Result<String, Box<EvalAltResult>> { Ok(si.name.clone()) });
|
||||
engine.register_get("quantity", |si: &mut SaleItem| -> Result<i64, Box<EvalAltResult>> { Ok(si.quantity as i64) });
|
||||
engine.register_get("unit_price", |si: &mut SaleItem| -> Result<f64, Box<EvalAltResult>> { Ok(si.unit_price) });
|
||||
engine.register_get("subtotal", |si: &mut SaleItem| -> Result<f64, Box<EvalAltResult>> { Ok(si.subtotal) });
|
||||
engine.register_get("service_active_until", |si: &mut SaleItem| -> Result<Option<i64>, Box<EvalAltResult>> { Ok(si.service_active_until) });
|
||||
|
||||
// Builder-style methods for SaleItem
|
||||
engine.register_type_with_name::<SaleItem>("SaleItem")
|
||||
.register_fn("product_id", |item: SaleItem, product_id_i64: i64| -> Result<SaleItem, Box<EvalAltResult>> { Ok(item.product_id(id_from_i64(product_id_i64)?)) })
|
||||
.register_fn("name", |item: SaleItem, name: String| -> Result<SaleItem, Box<EvalAltResult>> { Ok(item.name(name)) })
|
||||
.register_fn("quantity", |item: SaleItem, quantity_i64: i64| -> Result<SaleItem, Box<EvalAltResult>> { Ok(item.quantity(quantity_i64 as i32)) })
|
||||
.register_fn("unit_price", |item: SaleItem, unit_price: f64| -> Result<SaleItem, Box<EvalAltResult>> { Ok(item.unit_price(unit_price)) })
|
||||
.register_fn("subtotal", |item: SaleItem, subtotal: f64| -> Result<SaleItem, Box<EvalAltResult>> { Ok(item.subtotal(subtotal)) })
|
||||
.register_fn("service_active_until", |item: SaleItem, until: Option<i64>| -> Result<SaleItem, Box<EvalAltResult>> { Ok(item.service_active_until(until)) });
|
||||
|
||||
// --- Sale ---
|
||||
engine.register_type_with_name::<Sale>("Sale");
|
||||
engine.register_fn("new_sale", |company_id_i64: i64, buyer_name: String, buyer_email: String, total_amount: f64, status: SaleStatus, sale_date: i64| -> Result<Sale, Box<EvalAltResult>> { Ok(Sale::new(id_from_i64(company_id_i64)?, buyer_name, buyer_email, total_amount, status, sale_date)) });
|
||||
|
||||
// Getters for Sale
|
||||
engine.register_get("id", |s: &mut Sale| -> Result<i64, Box<EvalAltResult>> { Ok(s.get_id() as i64) });
|
||||
engine.register_get("customer_id", |s: &mut Sale| -> Result<i64, Box<EvalAltResult>> { Ok(s.company_id as i64) });
|
||||
engine.register_get("buyer_name", |s: &mut Sale| -> Result<String, Box<EvalAltResult>> { Ok(s.buyer_name.clone()) });
|
||||
engine.register_get("buyer_email", |s: &mut Sale| -> Result<String, Box<EvalAltResult>> { Ok(s.buyer_email.clone()) });
|
||||
engine.register_get("total_amount", |s: &mut Sale| -> Result<f64, Box<EvalAltResult>> { Ok(s.total_amount) });
|
||||
engine.register_get("status", |s: &mut Sale| -> Result<SaleStatus, Box<EvalAltResult>> { Ok(s.status.clone()) });
|
||||
engine.register_get("sale_date", |s: &mut Sale| -> Result<i64, Box<EvalAltResult>> { Ok(s.sale_date) });
|
||||
engine.register_get("items", |s: &mut Sale| -> Result<rhai::Array, Box<EvalAltResult>> {
|
||||
Ok(s.items.iter().cloned().map(Dynamic::from).collect::<rhai::Array>())
|
||||
});
|
||||
engine.register_get("notes", |s: &mut Sale| -> Result<String, Box<EvalAltResult>> { Ok(s.notes.clone()) });
|
||||
engine.register_get("created_at", |s: &mut Sale| -> Result<i64, Box<EvalAltResult>> { Ok(s.base_data.created_at) });
|
||||
engine.register_get("modified_at", |s: &mut Sale| -> Result<i64, Box<EvalAltResult>> { Ok(s.base_data.modified_at) });
|
||||
// engine.register_get("uuid", |s: &mut Sale| -> Result<Option<String>, Box<EvalAltResult>> { Ok(s.base_data().uuid.clone()) }); // UUID not in BaseModelData
|
||||
engine.register_get("comments", |s: &mut Sale| -> Result<rhai::Array, Box<EvalAltResult>> {
|
||||
Ok(s.base_data.comments.iter().map(|&id| Dynamic::from(id as i64)).collect::<rhai::Array>())
|
||||
});
|
||||
|
||||
// Builder-style methods for Sale
|
||||
engine.register_type_with_name::<Sale>("Sale")
|
||||
.register_fn("customer_id", |s: Sale, customer_id_i64: i64| -> Result<Sale, Box<EvalAltResult>> { Ok(s.company_id(id_from_i64(customer_id_i64)?)) })
|
||||
.register_fn("buyer_name", |s: Sale, buyer_name: String| -> Result<Sale, Box<EvalAltResult>> { Ok(s.buyer_name(buyer_name)) })
|
||||
.register_fn("buyer_email", |s: Sale, buyer_email: String| -> Result<Sale, Box<EvalAltResult>> { Ok(s.buyer_email(buyer_email)) })
|
||||
.register_fn("total_amount", |s: Sale, total_amount: f64| -> Result<Sale, Box<EvalAltResult>> { Ok(s.total_amount(total_amount)) })
|
||||
.register_fn("status", |s: Sale, status: SaleStatus| -> Result<Sale, Box<EvalAltResult>> { Ok(s.status(status)) })
|
||||
.register_fn("sale_date", |s: Sale, sale_date: i64| -> Result<Sale, Box<EvalAltResult>> { Ok(s.sale_date(sale_date)) })
|
||||
.register_fn("add_item", |s: Sale, item: SaleItem| -> Result<Sale, Box<EvalAltResult>> { Ok(s.add_item(item)) })
|
||||
.register_fn("items", |s: Sale, items: Vec<SaleItem>| -> Result<Sale, Box<EvalAltResult>> { Ok(s.items(items)) })
|
||||
.register_fn("notes", |s: Sale, notes: String| -> Result<Sale, Box<EvalAltResult>> { Ok(s.notes(notes)) })
|
||||
.register_fn("set_base_id", |s: Sale, id_i64: i64| -> Result<Sale, Box<EvalAltResult>> { Ok(s.set_base_id(id_from_i64(id_i64)?)) })
|
||||
// .register_fn("set_base_uuid", |s: Sale, uuid: Option<String>| -> Result<Sale, Box<EvalAltResult>> { Ok(s.set_base_uuid(uuid)) }) // UUID not in BaseModelData
|
||||
.register_fn("set_base_created_at", |s: Sale, time: i64| -> Result<Sale, Box<EvalAltResult>> { Ok(s.set_base_created_at(time)) })
|
||||
.register_fn("set_base_modified_at", |s: Sale, time: i64| -> Result<Sale, Box<EvalAltResult>> { Ok(s.set_base_modified_at(time)) })
|
||||
.register_fn("add_base_comment", |s: Sale, comment_id_i64: i64| -> Result<Sale, Box<EvalAltResult>> { Ok(s.add_base_comment(id_from_i64(comment_id_i64)?)) })
|
||||
.register_fn("set_base_comments", |s: Sale, comment_ids: Vec<i64>| -> Result<Sale, Box<EvalAltResult>> {
|
||||
let u32_ids = comment_ids.into_iter().map(id_from_i64).collect::<Result<Vec<u32>, _>>()?;
|
||||
Ok(s.set_base_comments(u32_ids))
|
||||
});
|
||||
|
||||
// DB functions for Product
|
||||
let captured_db_for_set_product = Arc::clone(&db);
|
||||
engine.register_fn("set_product", move |product: Product| -> Result<Product, Box<EvalAltResult>> {
|
||||
let original_id_for_error = product.get_id();
|
||||
captured_db_for_set_product.set(&product)
|
||||
.map(|(_id, updated_product)| updated_product)
|
||||
.map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set Product (Original ID: {}): {}", original_id_for_error, e).into(), Position::NONE))
|
||||
})
|
||||
});
|
||||
|
||||
let captured_db_for_get_prod = Arc::clone(&db);
|
||||
engine.register_fn("get_product_by_id", move |id_i64: i64| -> Result<Product, Box<EvalAltResult>> {
|
||||
let id_u32 = id_i64 as u32;
|
||||
captured_db_for_get_prod.get_by_id(id_u32)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting Product (ID: {}): {}", id_u32, e).into(), Position::NONE)))?
|
||||
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Product with ID {} not found", id_u32).into(), Position::NONE)))
|
||||
});
|
||||
|
||||
// DB functions for Sale
|
||||
let captured_db_for_set_sale = Arc::clone(&db);
|
||||
engine.register_fn("set_sale", move |sale: Sale| -> Result<Sale, Box<EvalAltResult>> {
|
||||
let original_id_for_error = sale.get_id();
|
||||
captured_db_for_set_sale.set(&sale)
|
||||
.map(|(_id, updated_sale)| updated_sale)
|
||||
.map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set Sale (Original ID: {}): {}", original_id_for_error, e).into(), Position::NONE))
|
||||
})
|
||||
});
|
||||
|
||||
let captured_db_for_get_sale = Arc::clone(&db);
|
||||
engine.register_fn("get_sale_by_id", move |id_i64: i64| -> Result<Sale, Box<EvalAltResult>> {
|
||||
let id_u32 = id_from_i64(id_i64)?;
|
||||
captured_db_for_get_sale.get_by_id(id_u32)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting Sale (ID: {}): {}", id_u32, e).into(), Position::NONE)))?
|
||||
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Sale with ID {} not found", id_u32).into(), Position::NONE)))
|
||||
});
|
||||
|
||||
// Mock DB functions for Shareholder
|
||||
let captured_db_for_set_shareholder = Arc::clone(&db);
|
||||
engine.register_fn("set_shareholder", move |shareholder: Shareholder| -> Result<Shareholder, Box<EvalAltResult>> {
|
||||
let original_id_for_error = shareholder.get_id();
|
||||
captured_db_for_set_shareholder.set(&shareholder)
|
||||
.map(|(_id, updated_shareholder)| updated_shareholder)
|
||||
.map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set Shareholder (Original ID: {}): {}", original_id_for_error, e).into(), Position::NONE))
|
||||
})
|
||||
});
|
||||
|
||||
let captured_db_for_get_sh = Arc::clone(&db);
|
||||
engine.register_fn("get_shareholder_by_id", move |id_i64: i64| -> Result<Shareholder, Box<EvalAltResult>> {
|
||||
let id_u32 = id_i64 as u32;
|
||||
captured_db_for_get_sh.get_by_id(id_u32)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting Shareholder (ID: {}): {}", id_u32, e).into(), Position::NONE)))?
|
||||
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Shareholder with ID {} not found", id_u32).into(), Position::NONE)))
|
||||
});
|
||||
|
||||
// Mock DB functions for Company
|
||||
let captured_db_for_set_company = Arc::clone(&db);
|
||||
engine.register_fn("set_company", move |company: Company| -> Result<Company, Box<EvalAltResult>> {
|
||||
let original_id_for_error = company.get_id(); // Capture ID before it's potentially changed by DB
|
||||
captured_db_for_set_company.set(&company)
|
||||
.map(|(_id, updated_company)| updated_company) // Use the model returned by db.set()
|
||||
.map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set Company (Original ID: {}): {}", original_id_for_error, e).into(), Position::NONE))
|
||||
})
|
||||
});
|
||||
|
||||
let captured_db_for_get = Arc::clone(&db);
|
||||
engine.register_fn("get_company_by_id", move |id_i64: i64| -> Result<Company, Box<EvalAltResult>> {
|
||||
let id_u32 = id_i64 as u32; // Assuming direct conversion is fine, or use a helper like in flow
|
||||
captured_db_for_get.get_by_id(id_u32)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting Company (ID: {}): {}", id_u32, e).into(), Position::NONE)))?
|
||||
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Company with ID {} not found", id_u32).into(), Position::NONE)))
|
||||
});
|
||||
|
||||
engine.register_global_module(module.into());
|
||||
}
|
202
heromodels/src/models/biz/sale.rs
Normal file
202
heromodels/src/models/biz/sale.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use heromodels_core::{BaseModelData, Model};
|
||||
|
||||
/// Represents the status of a sale.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum SaleStatus {
|
||||
Pending,
|
||||
Completed,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl Default for SaleStatus {
|
||||
fn default() -> Self {
|
||||
SaleStatus::Pending
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an individual item within a Sale.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SaleItem {
|
||||
pub product_id: u32,
|
||||
pub name: String, // Denormalized product name at time of sale
|
||||
pub quantity: i32,
|
||||
pub unit_price: f64, // Price per unit at time of sale
|
||||
pub subtotal: f64,
|
||||
pub service_active_until: Option<i64>, // Optional: For services, date until this specific purchased instance is active
|
||||
}
|
||||
|
||||
impl SaleItem {
|
||||
/// Creates a new `SaleItem`.
|
||||
pub fn new(product_id: u32, name: String, quantity: i32, unit_price: f64, subtotal: f64) -> Self {
|
||||
SaleItem {
|
||||
product_id,
|
||||
name,
|
||||
quantity,
|
||||
unit_price,
|
||||
subtotal,
|
||||
service_active_until: None,
|
||||
}
|
||||
}
|
||||
|
||||
// Builder methods
|
||||
pub fn product_id(mut self, product_id: u32) -> Self {
|
||||
self.product_id = product_id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn name(mut self, name: String) -> Self {
|
||||
self.name = name;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn quantity(mut self, quantity: i32) -> Self {
|
||||
self.quantity = quantity;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn unit_price(mut self, unit_price: f64) -> Self {
|
||||
self.unit_price = unit_price;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn subtotal(mut self, subtotal: f64) -> Self {
|
||||
self.subtotal = subtotal;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn service_active_until(mut self, service_active_until: Option<i64>) -> Self {
|
||||
self.service_active_until = service_active_until;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a sale of products or services.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Sale {
|
||||
pub base_data: BaseModelData,
|
||||
pub company_id: u32,
|
||||
pub buyer_name: String,
|
||||
pub buyer_email: String,
|
||||
pub total_amount: f64,
|
||||
pub status: SaleStatus,
|
||||
pub sale_date: i64,
|
||||
pub items: Vec<SaleItem>,
|
||||
pub notes: String,
|
||||
}
|
||||
|
||||
impl Model for Sale {
|
||||
fn db_prefix() -> &'static str {
|
||||
"sale"
|
||||
}
|
||||
|
||||
fn get_id(&self) -> u32 {
|
||||
self.base_data.id
|
||||
}
|
||||
|
||||
fn base_data_mut(&mut self) -> &mut BaseModelData {
|
||||
&mut self.base_data
|
||||
}
|
||||
}
|
||||
|
||||
impl Sale {
|
||||
/// Creates a new `Sale`.
|
||||
pub fn new(
|
||||
company_id: u32,
|
||||
buyer_name: String,
|
||||
buyer_email: String,
|
||||
total_amount: f64,
|
||||
status: SaleStatus,
|
||||
sale_date: i64,
|
||||
) -> Self {
|
||||
Sale {
|
||||
base_data: BaseModelData::new(),
|
||||
company_id,
|
||||
buyer_name,
|
||||
buyer_email,
|
||||
total_amount,
|
||||
status,
|
||||
sale_date,
|
||||
items: Vec::new(),
|
||||
notes: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// Builder methods for Sale
|
||||
pub fn company_id(mut self, company_id: u32) -> Self {
|
||||
self.company_id = company_id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn buyer_name(mut self, buyer_name: String) -> Self {
|
||||
self.buyer_name = buyer_name;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn buyer_email(mut self, buyer_email: String) -> Self {
|
||||
self.buyer_email = buyer_email;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn total_amount(mut self, total_amount: f64) -> Self {
|
||||
self.total_amount = total_amount;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn status(mut self, status: SaleStatus) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn sale_date(mut self, sale_date: i64) -> Self {
|
||||
self.sale_date = sale_date;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn items(mut self, items: Vec<SaleItem>) -> Self {
|
||||
self.items = items;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_item(mut self, item: SaleItem) -> Self {
|
||||
self.items.push(item);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn notes(mut self, notes: String) -> Self {
|
||||
self.notes = notes;
|
||||
self
|
||||
}
|
||||
|
||||
// Builder methods for BaseModelData fields, prefixed with base_
|
||||
pub fn set_base_id(mut self, id: u32) -> Self {
|
||||
self.base_data.id = id;
|
||||
self
|
||||
}
|
||||
|
||||
// UUID is not part of BaseModelData directly in heromodels_core
|
||||
// pub fn set_base_uuid(mut self, uuid: Option<String>) -> Self {
|
||||
// self.base_data.uuid = uuid; // Assuming uuid field exists if needed elsewhere
|
||||
// self
|
||||
// }
|
||||
|
||||
pub fn set_base_created_at(mut self, created_at: i64) -> Self {
|
||||
self.base_data.created_at = created_at;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_base_modified_at(mut self, modified_at: i64) -> Self {
|
||||
self.base_data.modified_at = modified_at;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_base_comment(mut self, comment_id: u32) -> Self {
|
||||
self.base_data.comments.push(comment_id);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_base_comments(mut self, comments: Vec<u32>) -> Self {
|
||||
self.base_data.comments = comments;
|
||||
self
|
||||
}
|
||||
}
|
102
heromodels/src/models/biz/shareholder.rs
Normal file
102
heromodels/src/models/biz/shareholder.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use heromodels_core::{BaseModelData, Model};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum ShareholderType {
|
||||
Individual,
|
||||
Corporate,
|
||||
}
|
||||
|
||||
impl Default for ShareholderType {
|
||||
fn default() -> Self {
|
||||
ShareholderType::Individual
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Shareholder {
|
||||
pub base_data: BaseModelData,
|
||||
pub company_id: u32,
|
||||
pub user_id: u32, // Or other entity ID
|
||||
pub name: String,
|
||||
pub shares: f64,
|
||||
pub percentage: f64,
|
||||
pub type_: ShareholderType,
|
||||
pub since: i64, // Timestamp
|
||||
}
|
||||
|
||||
impl Shareholder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
base_data: BaseModelData::new(),
|
||||
company_id: 0, // Default, to be set by builder
|
||||
user_id: 0, // Default, to be set by builder
|
||||
name: String::new(), // Default
|
||||
shares: 0.0, // Default
|
||||
percentage: 0.0, // Default
|
||||
type_: ShareholderType::default(), // Uses ShareholderType's Default impl
|
||||
since: 0, // Default timestamp, to be set by builder
|
||||
}
|
||||
}
|
||||
|
||||
// Builder methods
|
||||
pub fn company_id(mut self, company_id: u32) -> Self {
|
||||
self.company_id = company_id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn user_id(mut self, user_id: u32) -> Self {
|
||||
self.user_id = user_id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn name(mut self, name: String) -> Self {
|
||||
self.name = name;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn shares(mut self, shares: f64) -> Self {
|
||||
self.shares = shares;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn percentage(mut self, percentage: f64) -> Self {
|
||||
self.percentage = percentage;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn type_(mut self, type_: ShareholderType) -> Self {
|
||||
self.type_ = type_;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn since(mut self, since: i64) -> Self {
|
||||
self.since = since;
|
||||
self
|
||||
}
|
||||
|
||||
// Base data setters if needed for Rhai or specific scenarios
|
||||
pub fn set_base_created_at(mut self, created_at: i64) -> Self {
|
||||
self.base_data.created_at = created_at;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_base_modified_at(mut self, modified_at: i64) -> Self {
|
||||
self.base_data.modified_at = modified_at;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Model for Shareholder {
|
||||
fn db_prefix() -> &'static str {
|
||||
"shareholder"
|
||||
}
|
||||
|
||||
fn get_id(&self) -> u32 {
|
||||
self.base_data.id
|
||||
}
|
||||
|
||||
fn base_data_mut(&mut self) -> &mut BaseModelData {
|
||||
&mut self.base_data
|
||||
}
|
||||
}
|
200
heromodels/src/models/calendar/calendar.rs
Normal file
200
heromodels/src/models/calendar/calendar.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use heromodels_core::BaseModelData;
|
||||
use heromodels_derive::model;
|
||||
use rhai_autobind_macros::rhai_model_export;
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Represents the status of an attendee for an event
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum AttendanceStatus {
|
||||
Accepted,
|
||||
Declined,
|
||||
Tentative,
|
||||
NoResponse,
|
||||
}
|
||||
|
||||
/// Represents an attendee of an event
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)]
|
||||
pub struct Attendee {
|
||||
/// ID of the user attending
|
||||
// Assuming user_id might be queryable
|
||||
pub contact_id: u32,
|
||||
/// Attendance status of the user for the event
|
||||
pub status: AttendanceStatus,
|
||||
}
|
||||
|
||||
impl Attendee {
|
||||
pub fn new(contact_id: u32) -> Self {
|
||||
Self {
|
||||
contact_id,
|
||||
status: AttendanceStatus::NoResponse,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(mut self, status: AttendanceStatus) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an event in a calendar
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)]
|
||||
pub struct Event {
|
||||
/// Base model data
|
||||
#[serde(flatten)]
|
||||
pub base_data: BaseModelData,
|
||||
/// Title of the event
|
||||
pub title: String,
|
||||
/// Optional description of the event
|
||||
pub description: Option<String>,
|
||||
/// Start time of the event
|
||||
pub start_time: DateTime<Utc>,
|
||||
/// End time of the event
|
||||
pub end_time: DateTime<Utc>,
|
||||
/// List of attendees for the event
|
||||
pub attendees: Vec<Attendee>,
|
||||
/// Optional location of the event
|
||||
pub location: Option<String>,
|
||||
}
|
||||
|
||||
impl Event {
|
||||
/// Creates a new event
|
||||
pub fn new(title: impl ToString, start_time: DateTime<Utc>, end_time: DateTime<Utc>) -> Self {
|
||||
Self {
|
||||
base_data: BaseModelData::new(),
|
||||
title: title.to_string(),
|
||||
description: None,
|
||||
start_time,
|
||||
end_time,
|
||||
attendees: Vec::new(),
|
||||
location: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the title for the event
|
||||
pub fn title(mut self, title: impl ToString) -> Self {
|
||||
self.title = title.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the description for the event
|
||||
pub fn description(mut self, description: impl ToString) -> Self {
|
||||
self.description = Some(description.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the location for the event
|
||||
pub fn location(mut self, location: impl ToString) -> Self {
|
||||
self.location = Some(location.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an attendee to the event
|
||||
pub fn add_attendee(mut self, attendee: Attendee) -> Self {
|
||||
// Prevent duplicate attendees by contact_id
|
||||
if !self.attendees.iter().any(|a| a.contact_id == attendee.contact_id) {
|
||||
self.attendees.push(attendee);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Removes an attendee from the event by user_id
|
||||
pub fn remove_attendee(mut self, contact_id: u32) -> Self {
|
||||
self.attendees.retain(|a| a.contact_id != contact_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Updates the status of an existing attendee
|
||||
pub fn update_attendee_status(mut self, contact_id: u32, status: AttendanceStatus) -> Self {
|
||||
if let Some(attendee) = self.attendees.iter_mut().find(|a| a.contact_id == contact_id) {
|
||||
attendee.status = status;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Reschedules the event to new start and end times
|
||||
pub fn reschedule(
|
||||
mut self,
|
||||
new_start_time: DateTime<Utc>,
|
||||
new_end_time: DateTime<Utc>,
|
||||
) -> Self {
|
||||
// Basic validation: end_time should be after start_time
|
||||
if new_end_time > new_start_time {
|
||||
self.start_time = new_start_time;
|
||||
self.end_time = new_end_time;
|
||||
}
|
||||
// Optionally, add error handling or return a Result type
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a calendar with events
|
||||
#[rhai_model_export(
|
||||
db_type = "std::sync::Arc<crate::db::hero::OurDB>",
|
||||
)]
|
||||
#[model]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)]
|
||||
pub struct Calendar {
|
||||
/// Base model data
|
||||
#[serde(flatten)]
|
||||
pub base_data: BaseModelData,
|
||||
|
||||
/// Name of the calendar
|
||||
pub name: String,
|
||||
|
||||
/// Optional description of the calendar
|
||||
pub description: Option<String>,
|
||||
|
||||
/// List of events in the calendar
|
||||
// For now, events are embedded. If they become separate models, this would be Vec<[IDType]>.
|
||||
pub events: Vec<i64>,
|
||||
}
|
||||
|
||||
impl Calendar {
|
||||
/// Creates a new calendar with auto-generated ID
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - Optional ID for the calendar (use None for auto-generated ID)
|
||||
/// * `name` - Name of the calendar
|
||||
pub fn new(id: Option<u32>, name: impl ToString) -> Self {
|
||||
let mut base_data = BaseModelData::new();
|
||||
if let Some(id) = id {
|
||||
base_data.update_id(id);
|
||||
}
|
||||
|
||||
Self {
|
||||
base_data,
|
||||
name: name.to_string(),
|
||||
description: None,
|
||||
events: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the name for the calendar
|
||||
pub fn name(mut self, name: impl ToString) -> Self {
|
||||
self.name = name.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the description for the calendar
|
||||
pub fn description(mut self, description: impl ToString) -> Self {
|
||||
self.description = Some(description.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds an event to the calendar
|
||||
pub fn add_event(mut self, event_id: i64) -> Self {
|
||||
// Prevent duplicate events by id
|
||||
if !self.events.iter().any(|e_id| *e_id == event_id) {
|
||||
self.events.push(event_id);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Removes an event from the calendar by its ID
|
||||
pub fn remove_event(mut self, event_id_to_remove: i64) -> Self {
|
||||
self.events.retain(|&event_id_in_vec| event_id_in_vec != event_id_to_remove);
|
||||
self
|
||||
}
|
||||
}
|
7
heromodels/src/models/calendar/mod.rs
Normal file
7
heromodels/src/models/calendar/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
// Export calendar module
|
||||
pub mod calendar;
|
||||
pub mod rhai;
|
||||
|
||||
// Re-export Calendar, Event, Attendee, and AttendanceStatus from the inner calendar module (calendar.rs) within src/models/calendar/mod.rs
|
||||
pub use self::calendar::{Calendar, Event, Attendee, AttendanceStatus};
|
||||
pub use rhai::register_rhai_engine_functions as register_calendar_rhai_module;
|
90
heromodels/src/models/calendar/rhai.rs
Normal file
90
heromodels/src/models/calendar/rhai.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
use rhai::{Engine, EvalAltResult, NativeCallContext, ImmutableString};
|
||||
use std::sync::Arc;
|
||||
|
||||
use heromodels_core::BaseModelData;
|
||||
use crate::db::hero::OurDB;
|
||||
use super::calendar::{Calendar, Event, Attendee, AttendanceStatus};
|
||||
use adapter_macros::{adapt_rhai_i64_input_fn, adapt_rhai_i64_input_method};
|
||||
use adapter_macros::rhai_timestamp_helpers;
|
||||
|
||||
// Helper function for get_all_calendars registration
|
||||
|
||||
|
||||
fn new_calendar_rhai(name: String) -> Result<Calendar, Box<EvalAltResult>> {
|
||||
Ok(Calendar::new(None, name))
|
||||
}
|
||||
|
||||
fn new_event_rhai(
|
||||
context: NativeCallContext,
|
||||
title_rhai: ImmutableString,
|
||||
start_time_ts: i64,
|
||||
end_time_ts: i64,
|
||||
) -> Result<Event, Box<EvalAltResult>> {
|
||||
let start_time = rhai_timestamp_helpers::rhai_timestamp_to_datetime(start_time_ts)
|
||||
.map_err(|e_str| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to convert start_time for Event: {}", e_str).into(),
|
||||
context.position(),
|
||||
)))?;
|
||||
|
||||
let end_time = rhai_timestamp_helpers::rhai_timestamp_to_datetime(end_time_ts)
|
||||
.map_err(|e_str| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to convert end_time for Event: {}", e_str).into(),
|
||||
context.position(),
|
||||
)))?;
|
||||
|
||||
Ok(Event::new(title_rhai.to_string(), start_time, end_time))
|
||||
}
|
||||
|
||||
pub fn register_rhai_engine_functions(engine: &mut Engine, db: Arc<OurDB>) {
|
||||
engine.register_fn("name", move |calendar: Calendar, name: String| Calendar::name(calendar, name));
|
||||
engine.register_fn("description", move |calendar: Calendar, description: String| Calendar::description(calendar, description));
|
||||
engine.register_fn("add_event", Calendar::add_event);
|
||||
// Note: Event IDs are i64 in Calendar.events, but Event model's base_data.id is u32.
|
||||
// This might require adjustment if events are fetched by ID from the DB via Calendar.events.
|
||||
|
||||
engine.register_fn("new_event", |context: NativeCallContext, title_rhai: ImmutableString, start_time_ts: i64, end_time_ts: i64| -> Result<Event, Box<EvalAltResult>> {
|
||||
new_event_rhai(context, title_rhai, start_time_ts, end_time_ts)
|
||||
});
|
||||
engine.register_fn("title", move |event: Event, title: String| Event::title(event, title));
|
||||
engine.register_fn("description", move |event: Event, description: String| Event::description(event, description));
|
||||
engine.register_fn("add_attendee", move |event: Event, attendee: Attendee| Event::add_attendee(event, attendee));
|
||||
engine.register_fn("remove_attendee", adapt_rhai_i64_input_method!(Event, remove_attendee, u32));
|
||||
engine.register_fn("update_attendee_status", move |context: NativeCallContext, event: Event, contact_id_i64: i64, status: AttendanceStatus| -> Result<Event, Box<EvalAltResult>> {
|
||||
let contact_id_u32: u32 = contact_id_i64.try_into().map_err(|_e| {
|
||||
Box::new(EvalAltResult::ErrorArithmetic(
|
||||
format!("Conversion error for contact_id in Event::update_attendee_status from i64 to u32"),
|
||||
context.position(),
|
||||
))
|
||||
})?;
|
||||
Ok(event.update_attendee_status(contact_id_u32, status))
|
||||
});
|
||||
|
||||
engine.register_fn("new_attendee", adapt_rhai_i64_input_fn!(Attendee::new, u32));
|
||||
|
||||
engine.register_fn("new_calendar", |name: String| -> Result<Calendar, Box<EvalAltResult>> { new_calendar_rhai(name) });
|
||||
|
||||
// Register a function to get the database instance
|
||||
engine.register_fn("get_db", move || db.clone());
|
||||
|
||||
// Register getters for Calendar
|
||||
engine.register_get("id", |c: &mut Calendar| -> Result<i64, Box<EvalAltResult>> { Ok(c.base_data.id as i64) });
|
||||
engine.register_get("name", |c: &mut Calendar| -> Result<String, Box<EvalAltResult>> {
|
||||
// println!("Rhai attempting to get Calendar.name: {}", c.name); // Debug print
|
||||
Ok(c.name.clone())
|
||||
});
|
||||
engine.register_get("description", |c: &mut Calendar| -> Result<Option<String>, Box<EvalAltResult>> { Ok(c.description.clone()) });
|
||||
|
||||
// Register getter for Calendar.base_data
|
||||
engine.register_get("base_data", |c: &mut Calendar| -> Result<BaseModelData, Box<EvalAltResult>> { Ok(c.base_data.clone()) });
|
||||
|
||||
// Register getters for BaseModelData
|
||||
engine.register_get("id", |bmd: &mut BaseModelData| -> Result<i64, Box<EvalAltResult>> { Ok(bmd.id.into()) });
|
||||
|
||||
// Database interaction functions for Calendar are expected to be provided by #[rhai_model_export(..)] on the Calendar struct.
|
||||
// Ensure that `db.rs` or similar correctly wires up the `OurDB` methods for these.
|
||||
|
||||
// Getters for Event
|
||||
engine.register_get("id", |e: &mut Event| -> Result<i64, Box<EvalAltResult>> { Ok(e.base_data.id as i64) });
|
||||
engine.register_get("title", |e: &mut Event| -> Result<String, Box<EvalAltResult>> { Ok(e.title.clone()) });
|
||||
// Add more getters for Event fields as needed
|
||||
}
|
@@ -13,10 +13,10 @@ pub struct Comment {
|
||||
}
|
||||
|
||||
impl Comment {
|
||||
/// Create a new comment
|
||||
pub fn new(id: u32) -> Self {
|
||||
/// Create a new comment with auto-generated ID
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
base_data: BaseModelData::new(id),
|
||||
base_data: BaseModelData::new(),
|
||||
user_id: 0,
|
||||
content: String::new(),
|
||||
}
|
||||
|
99
heromodels/src/models/finance/account.rs
Normal file
99
heromodels/src/models/finance/account.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
// heromodels/src/models/finance/account.rs
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use heromodels_derive::model;
|
||||
use heromodels_core::BaseModelData;
|
||||
|
||||
use super::asset::Asset;
|
||||
|
||||
/// Account represents a financial account owned by a user
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
|
||||
#[model] // Has base.Base in V spec
|
||||
pub struct Account {
|
||||
pub base_data: BaseModelData,
|
||||
pub name: String, // internal name of the account for the user
|
||||
pub user_id: u32, // user id of the owner of the account
|
||||
pub description: String, // optional description of the account
|
||||
pub ledger: String, // describes the ledger/blockchain where the account is located
|
||||
pub address: String, // address of the account on the blockchain
|
||||
pub pubkey: String, // public key
|
||||
pub assets: Vec<Asset>, // list of assets in this account
|
||||
}
|
||||
|
||||
impl Account {
|
||||
/// Create a new account with auto-generated ID
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - Optional ID for the account (use None for auto-generated ID)
|
||||
/// * `name` - Name of the account
|
||||
/// * `user_id` - ID of the user who owns the account
|
||||
/// * `description` - Description of the account
|
||||
/// * `ledger` - Ledger/blockchain where the account is located
|
||||
/// * `address` - Address of the account on the blockchain
|
||||
/// * `pubkey` - Public key
|
||||
pub fn new(
|
||||
id: Option<u32>,
|
||||
name: impl ToString,
|
||||
user_id: u32,
|
||||
description: impl ToString,
|
||||
ledger: impl ToString,
|
||||
address: impl ToString,
|
||||
pubkey: impl ToString,
|
||||
) -> Self {
|
||||
let mut base_data = BaseModelData::new();
|
||||
if let Some(id) = id {
|
||||
base_data.update_id(id);
|
||||
}
|
||||
|
||||
Self {
|
||||
base_data,
|
||||
name: name.to_string(),
|
||||
user_id,
|
||||
description: description.to_string(),
|
||||
ledger: ledger.to_string(),
|
||||
address: address.to_string(),
|
||||
pubkey: pubkey.to_string(),
|
||||
assets: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add an asset to the account
|
||||
pub fn add_asset(mut self, asset: Asset) -> Self {
|
||||
self.assets.push(asset);
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the total value of all assets in the account
|
||||
pub fn total_value(&self) -> f64 {
|
||||
self.assets.iter().map(|asset| asset.amount).sum()
|
||||
}
|
||||
|
||||
/// Find an asset by name
|
||||
pub fn find_asset_by_name(&self, name: &str) -> Option<&Asset> {
|
||||
self.assets.iter().find(|asset| asset.name == name)
|
||||
}
|
||||
|
||||
/// Update the account details
|
||||
pub fn update_details(
|
||||
mut self,
|
||||
name: Option<impl ToString>,
|
||||
description: Option<impl ToString>,
|
||||
address: Option<impl ToString>,
|
||||
pubkey: Option<impl ToString>,
|
||||
) -> Self {
|
||||
if let Some(name) = name {
|
||||
self.name = name.to_string();
|
||||
}
|
||||
if let Some(description) = description {
|
||||
self.description = description.to_string();
|
||||
}
|
||||
if let Some(address) = address {
|
||||
self.address = address.to_string();
|
||||
}
|
||||
if let Some(pubkey) = pubkey {
|
||||
self.pubkey = pubkey.to_string();
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
100
heromodels/src/models/finance/asset.rs
Normal file
100
heromodels/src/models/finance/asset.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
// heromodels/src/models/finance/asset.rs
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use heromodels_derive::model;
|
||||
use heromodels_core::BaseModelData;
|
||||
|
||||
/// AssetType defines the type of blockchain asset
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum AssetType {
|
||||
Erc20, // ERC-20 token standard
|
||||
Erc721, // ERC-721 NFT standard
|
||||
Erc1155, // ERC-1155 Multi-token standard
|
||||
Native, // Native blockchain asset (e.g., ETH, BTC)
|
||||
}
|
||||
|
||||
impl Default for AssetType {
|
||||
fn default() -> Self {
|
||||
AssetType::Native
|
||||
}
|
||||
}
|
||||
|
||||
/// Asset represents a digital asset or token
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
|
||||
#[model] // Has base.Base in V spec
|
||||
pub struct Asset {
|
||||
pub base_data: BaseModelData,
|
||||
pub name: String, // Name of the asset
|
||||
pub description: String, // Description of the asset
|
||||
pub amount: f64, // Amount of the asset
|
||||
pub address: String, // Address of the asset on the blockchain or bank
|
||||
pub asset_type: AssetType, // Type of the asset
|
||||
pub decimals: u8, // Number of decimals of the asset
|
||||
}
|
||||
|
||||
impl Asset {
|
||||
/// Create a new asset with auto-generated ID
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - Optional ID for the asset (use None for auto-generated ID)
|
||||
/// * `name` - Name of the asset
|
||||
/// * `description` - Description of the asset
|
||||
/// * `amount` - Amount of the asset
|
||||
/// * `address` - Address of the asset on the blockchain or bank
|
||||
/// * `asset_type` - Type of the asset
|
||||
/// * `decimals` - Number of decimals of the asset
|
||||
pub fn new(
|
||||
id: Option<u32>,
|
||||
name: impl ToString,
|
||||
description: impl ToString,
|
||||
amount: f64,
|
||||
address: impl ToString,
|
||||
asset_type: AssetType,
|
||||
decimals: u8,
|
||||
) -> Self {
|
||||
let mut base_data = BaseModelData::new();
|
||||
if let Some(id) = id {
|
||||
base_data.update_id(id);
|
||||
}
|
||||
|
||||
Self {
|
||||
base_data,
|
||||
name: name.to_string(),
|
||||
description: description.to_string(),
|
||||
amount,
|
||||
address: address.to_string(),
|
||||
asset_type,
|
||||
decimals,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the asset amount
|
||||
pub fn update_amount(mut self, amount: f64) -> Self {
|
||||
self.amount = amount;
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the formatted amount with proper decimal places
|
||||
pub fn formatted_amount(&self) -> String {
|
||||
let factor = 10_f64.powi(self.decimals as i32);
|
||||
let formatted_amount = (self.amount * factor).round() / factor;
|
||||
format!("{:.1$}", formatted_amount, self.decimals as usize)
|
||||
}
|
||||
|
||||
/// Transfer amount to another asset
|
||||
pub fn transfer_to(&mut self, target: &mut Asset, amount: f64) -> Result<(), &'static str> {
|
||||
if amount <= 0.0 {
|
||||
return Err("Transfer amount must be positive");
|
||||
}
|
||||
|
||||
if self.amount < amount {
|
||||
return Err("Insufficient balance for transfer");
|
||||
}
|
||||
|
||||
self.amount -= amount;
|
||||
target.amount += amount;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
312
heromodels/src/models/finance/marketplace.rs
Normal file
312
heromodels/src/models/finance/marketplace.rs
Normal file
@@ -0,0 +1,312 @@
|
||||
// heromodels/src/models/finance/marketplace.rs
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use chrono::{DateTime, Utc};
|
||||
use heromodels_core::BaseModelData;
|
||||
use heromodels_derive::model;
|
||||
|
||||
use super::asset::AssetType;
|
||||
|
||||
/// ListingStatus defines the status of a marketplace listing
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ListingStatus {
|
||||
Active, // Listing is active and available
|
||||
Sold, // Listing has been sold
|
||||
Cancelled, // Listing was cancelled by the seller
|
||||
Expired, // Listing has expired
|
||||
}
|
||||
|
||||
impl Default for ListingStatus {
|
||||
fn default() -> Self {
|
||||
ListingStatus::Active
|
||||
}
|
||||
}
|
||||
|
||||
/// ListingType defines the type of marketplace listing
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ListingType {
|
||||
FixedPrice, // Fixed price sale
|
||||
Auction, // Auction with bids
|
||||
Exchange, // Exchange for other assets
|
||||
}
|
||||
|
||||
impl Default for ListingType {
|
||||
fn default() -> Self {
|
||||
ListingType::FixedPrice
|
||||
}
|
||||
}
|
||||
|
||||
/// BidStatus defines the status of a bid on an auction listing
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum BidStatus {
|
||||
Active, // Bid is active
|
||||
Accepted, // Bid was accepted
|
||||
Rejected, // Bid was rejected
|
||||
Cancelled, // Bid was cancelled by the bidder
|
||||
}
|
||||
|
||||
impl Default for BidStatus {
|
||||
fn default() -> Self {
|
||||
BidStatus::Active
|
||||
}
|
||||
}
|
||||
|
||||
/// Bid represents a bid on an auction listing
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
|
||||
pub struct Bid {
|
||||
pub listing_id: String, // ID of the listing this bid belongs to
|
||||
pub bidder_id: u32, // ID of the user who placed the bid
|
||||
pub amount: f64, // Bid amount
|
||||
pub currency: String, // Currency of the bid
|
||||
pub status: BidStatus, // Status of the bid
|
||||
pub created_at: DateTime<Utc>, // When the bid was created
|
||||
}
|
||||
|
||||
impl Bid {
|
||||
/// Create a new bid
|
||||
pub fn new(
|
||||
listing_id: impl ToString,
|
||||
bidder_id: u32,
|
||||
amount: f64,
|
||||
currency: impl ToString,
|
||||
) -> Self {
|
||||
Self {
|
||||
listing_id: listing_id.to_string(),
|
||||
bidder_id,
|
||||
amount,
|
||||
currency: currency.to_string(),
|
||||
status: BidStatus::default(),
|
||||
created_at: Utc::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the status of the bid
|
||||
pub fn update_status(mut self, status: BidStatus) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Listing represents a marketplace listing for an asset
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
|
||||
#[model] // Has base.Base in V spec
|
||||
pub struct Listing {
|
||||
pub base_data: BaseModelData,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub asset_id: String,
|
||||
pub asset_type: AssetType,
|
||||
pub seller_id: String,
|
||||
pub price: f64, // Initial price for fixed price, or starting price for auction
|
||||
pub currency: String,
|
||||
pub listing_type: ListingType,
|
||||
pub status: ListingStatus,
|
||||
pub expires_at: Option<DateTime<Utc>>, // Optional expiration date
|
||||
pub sold_at: Option<DateTime<Utc>>, // Optional date when the item was sold
|
||||
pub buyer_id: Option<String>, // Optional buyer ID
|
||||
pub sale_price: Option<f64>, // Optional final sale price
|
||||
pub bids: Vec<Bid>, // List of bids for auction type listings
|
||||
pub tags: Vec<String>, // Tags for the listing
|
||||
pub image_url: Option<String>, // Optional image URL
|
||||
}
|
||||
|
||||
impl Listing {
|
||||
/// Create a new listing with auto-generated ID
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - Optional ID for the listing (use None for auto-generated ID)
|
||||
/// * `title` - Title of the listing
|
||||
/// * `description` - Description of the listing
|
||||
/// * `asset_id` - ID of the asset being listed
|
||||
/// * `asset_type` - Type of the asset
|
||||
/// * `seller_id` - ID of the seller
|
||||
/// * `price` - Initial price for fixed price, or starting price for auction
|
||||
/// * `currency` - Currency of the price
|
||||
/// * `listing_type` - Type of the listing
|
||||
/// * `expires_at` - Optional expiration date
|
||||
/// * `tags` - Tags for the listing
|
||||
/// * `image_url` - Optional image URL
|
||||
pub fn new(
|
||||
id: Option<u32>,
|
||||
title: impl ToString,
|
||||
description: impl ToString,
|
||||
asset_id: impl ToString,
|
||||
asset_type: AssetType,
|
||||
seller_id: impl ToString,
|
||||
price: f64,
|
||||
currency: impl ToString,
|
||||
listing_type: ListingType,
|
||||
expires_at: Option<DateTime<Utc>>,
|
||||
tags: Vec<String>,
|
||||
image_url: Option<impl ToString>,
|
||||
) -> Self {
|
||||
let mut base_data = BaseModelData::new();
|
||||
if let Some(id) = id {
|
||||
base_data.update_id(id);
|
||||
}
|
||||
|
||||
Self {
|
||||
base_data,
|
||||
title: title.to_string(),
|
||||
description: description.to_string(),
|
||||
asset_id: asset_id.to_string(),
|
||||
asset_type,
|
||||
seller_id: seller_id.to_string(),
|
||||
price,
|
||||
currency: currency.to_string(),
|
||||
listing_type,
|
||||
status: ListingStatus::default(),
|
||||
expires_at,
|
||||
sold_at: None,
|
||||
buyer_id: None,
|
||||
sale_price: None,
|
||||
bids: Vec::new(),
|
||||
tags,
|
||||
image_url: image_url.map(|url| url.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a bid to an auction listing
|
||||
pub fn add_bid(mut self, bid: Bid) -> Result<Self, &'static str> {
|
||||
// Check if listing is an auction
|
||||
if self.listing_type != ListingType::Auction {
|
||||
return Err("Bids can only be placed on auction listings");
|
||||
}
|
||||
|
||||
// Check if listing is active
|
||||
if self.status != ListingStatus::Active {
|
||||
return Err("Cannot place bid on inactive listing");
|
||||
}
|
||||
|
||||
// Check if bid amount is higher than current price
|
||||
if bid.amount <= self.price {
|
||||
return Err("Bid amount must be higher than current price");
|
||||
}
|
||||
|
||||
// Check if there are existing bids and if the new bid is higher
|
||||
if let Some(highest_bid) = self.highest_bid() {
|
||||
if bid.amount <= highest_bid.amount {
|
||||
return Err("Bid amount must be higher than current highest bid");
|
||||
}
|
||||
}
|
||||
|
||||
// Add the bid
|
||||
self.bids.push(bid);
|
||||
|
||||
// Update the current price to the new highest bid
|
||||
if let Some(highest_bid) = self.highest_bid() {
|
||||
self.price = highest_bid.amount;
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Get the highest active bid
|
||||
pub fn highest_bid(&self) -> Option<&Bid> {
|
||||
self.bids
|
||||
.iter()
|
||||
.filter(|bid| bid.status == BidStatus::Active)
|
||||
.max_by(|a, b| a.amount.partial_cmp(&b.amount).unwrap())
|
||||
}
|
||||
|
||||
/// Complete a sale (fixed price or auction)
|
||||
pub fn complete_sale(
|
||||
mut self,
|
||||
buyer_id: impl ToString,
|
||||
sale_price: f64,
|
||||
) -> Result<Self, &'static str> {
|
||||
if self.status != ListingStatus::Active {
|
||||
return Err("Cannot complete sale for inactive listing");
|
||||
}
|
||||
|
||||
self.status = ListingStatus::Sold;
|
||||
self.buyer_id = Some(buyer_id.to_string());
|
||||
self.sale_price = Some(sale_price);
|
||||
self.sold_at = Some(Utc::now());
|
||||
|
||||
// If this was an auction, accept the winning bid and reject others
|
||||
if self.listing_type == ListingType::Auction {
|
||||
for bid in &mut self.bids {
|
||||
if bid.bidder_id.to_string() == self.buyer_id.as_ref().unwrap().to_string()
|
||||
&& bid.amount == sale_price
|
||||
{
|
||||
bid.status = BidStatus::Accepted;
|
||||
} else {
|
||||
bid.status = BidStatus::Rejected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Cancel the listing
|
||||
pub fn cancel(mut self) -> Result<Self, &'static str> {
|
||||
if self.status != ListingStatus::Active {
|
||||
return Err("Cannot cancel inactive listing");
|
||||
}
|
||||
|
||||
self.status = ListingStatus::Cancelled;
|
||||
|
||||
// Cancel all active bids
|
||||
for bid in &mut self.bids {
|
||||
if bid.status == BidStatus::Active {
|
||||
bid.status = BidStatus::Cancelled;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Check if the listing has expired and update status if needed
|
||||
pub fn check_expiration(mut self) -> Self {
|
||||
if self.status == ListingStatus::Active {
|
||||
if let Some(expires_at) = self.expires_at {
|
||||
if Utc::now() > expires_at {
|
||||
self.status = ListingStatus::Expired;
|
||||
|
||||
// Cancel all active bids
|
||||
for bid in &mut self.bids {
|
||||
if bid.status == BidStatus::Active {
|
||||
bid.status = BidStatus::Cancelled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Add tags to the listing
|
||||
pub fn add_tags(mut self, tags: Vec<impl ToString>) -> Self {
|
||||
for tag in tags {
|
||||
self.tags.push(tag.to_string());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Update the listing details
|
||||
pub fn update_details(
|
||||
mut self,
|
||||
title: Option<impl ToString>,
|
||||
description: Option<impl ToString>,
|
||||
price: Option<f64>,
|
||||
image_url: Option<impl ToString>,
|
||||
) -> Self {
|
||||
if let Some(title) = title {
|
||||
self.title = title.to_string();
|
||||
}
|
||||
if let Some(description) = description {
|
||||
self.description = description.to_string();
|
||||
}
|
||||
if let Some(price) = price {
|
||||
self.price = price;
|
||||
}
|
||||
if let Some(image_url) = image_url {
|
||||
self.image_url = Some(image_url.to_string());
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
12
heromodels/src/models/finance/mod.rs
Normal file
12
heromodels/src/models/finance/mod.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
// heromodels/src/models/finance/mod.rs
|
||||
// This module contains finance-related models: Account, Asset, and Marketplace
|
||||
|
||||
pub mod account;
|
||||
pub mod asset;
|
||||
pub mod marketplace;
|
||||
pub mod rhai;
|
||||
|
||||
// Re-export main models for easier access
|
||||
pub use self::account::Account;
|
||||
pub use self::asset::{Asset, AssetType};
|
||||
pub use self::marketplace::{Listing, ListingStatus, ListingType, Bid, BidStatus};
|
416
heromodels/src/models/finance/rhai.rs
Normal file
416
heromodels/src/models/finance/rhai.rs
Normal file
@@ -0,0 +1,416 @@
|
||||
use rhai::{Engine, Array, Dynamic, ImmutableString, INT, EvalAltResult, NativeCallContext};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error as StdError; // For Box<dyn StdError>
|
||||
|
||||
// Custom error type for Rhai that wraps a String
|
||||
#[derive(Debug)]
|
||||
struct RhaiStringError(String);
|
||||
|
||||
impl std::fmt::Display for RhaiStringError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for RhaiStringError {}
|
||||
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use crate::models::finance::account::Account;
|
||||
use crate::models::finance::asset::{Asset, AssetType};
|
||||
use crate::models::finance::marketplace::{Listing, Bid, ListingStatus, ListingType, BidStatus};
|
||||
|
||||
// --- Enum to String & String to Enum Helper Functions (domain-specific) ---
|
||||
// These remain here as they are specific to the finance models' enums.
|
||||
|
||||
fn asset_type_to_string(asset_type: &AssetType) -> ImmutableString {
|
||||
format!("{:?}", asset_type).into()
|
||||
}
|
||||
fn string_to_asset_type(s: &str) -> Result<AssetType, Box<EvalAltResult>> {
|
||||
match s {
|
||||
"Erc20" => Ok(AssetType::Erc20),
|
||||
"Erc721" => Ok(AssetType::Erc721),
|
||||
"Erc1155" => Ok(AssetType::Erc1155),
|
||||
"Native" => Ok(AssetType::Native),
|
||||
_ => Err(format!("Invalid AssetType string: {}", s).into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn listing_status_to_string(status: &ListingStatus) -> ImmutableString {
|
||||
format!("{:?}", status).into()
|
||||
}
|
||||
fn string_to_listing_status(s: &str) -> Result<ListingStatus, Box<EvalAltResult>> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"active" => Ok(ListingStatus::Active),
|
||||
"sold" => Ok(ListingStatus::Sold),
|
||||
"cancelled" => Ok(ListingStatus::Cancelled),
|
||||
"expired" => Ok(ListingStatus::Expired),
|
||||
_ => Err(format!("Invalid ListingStatus string: {}", s).into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn listing_type_to_string(lt: &ListingType) -> ImmutableString {
|
||||
format!("{:?}", lt).into()
|
||||
}
|
||||
fn string_to_listing_type(s: &str) -> Result<ListingType, Box<EvalAltResult>> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"fixedprice" => Ok(ListingType::FixedPrice),
|
||||
"auction" => Ok(ListingType::Auction),
|
||||
"exchange" => Ok(ListingType::Exchange),
|
||||
_ => Err(format!("Invalid ListingType string: {}", s).into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn bid_status_to_string(status: &BidStatus) -> ImmutableString {
|
||||
format!("{:?}", status).into()
|
||||
}
|
||||
fn string_to_bid_status(s: &str) -> Result<BidStatus, Box<EvalAltResult>> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"active" => Ok(BidStatus::Active),
|
||||
"accepted" => Ok(BidStatus::Accepted),
|
||||
"rejected" => Ok(BidStatus::Rejected),
|
||||
"cancelled" => Ok(BidStatus::Cancelled),
|
||||
_ => Err(format!("Invalid BidStatus string: {}", s).into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_rhai_engine_functions(
|
||||
engine: &mut Engine,
|
||||
db_accounts: Arc<Mutex<HashMap<u32, Account>>>,
|
||||
db_assets: Arc<Mutex<HashMap<u32, Asset>>>,
|
||||
db_listings: Arc<Mutex<HashMap<u32, Listing>>>,
|
||||
) {
|
||||
// --- Account model ---
|
||||
engine.register_type_with_name::<Account>("Account");
|
||||
engine.register_fn("new_account", || -> Account {
|
||||
Account::new(None, "", 0, "", "", "", "")
|
||||
});
|
||||
// Getters
|
||||
engine.register_get("id", |acc: &mut Account| acc.base_data.id as INT);
|
||||
engine.register_get("created_at_ts", |acc: &mut Account| acc.base_data.created_at);
|
||||
engine.register_get("name", |acc: &mut Account| -> ImmutableString { acc.name.clone().into() });
|
||||
engine.register_get("user_id", |acc: &mut Account| acc.user_id as INT);
|
||||
engine.register_get("description", |acc: &mut Account| -> ImmutableString { acc.description.clone().into() });
|
||||
engine.register_get("ledger", |acc: &mut Account| -> ImmutableString { acc.ledger.clone().into() });
|
||||
engine.register_get("address", |acc: &mut Account| -> ImmutableString { acc.address.clone().into() });
|
||||
engine.register_get("pubkey", |acc: &mut Account| -> ImmutableString { acc.pubkey.clone().into() });
|
||||
engine.register_get("assets_list", |acc: &mut Account| -> Result<Array, Box<EvalAltResult>> {
|
||||
Ok(acc.assets.iter().cloned().map(rhai::Dynamic::from).collect())
|
||||
});
|
||||
engine.register_get("modified_at_ts", |acc: &mut Account| acc.base_data.modified_at);
|
||||
|
||||
// Setters (Builder Pattern)
|
||||
engine.register_fn("set_name", |mut acc: Account, name: ImmutableString| -> Result<Account, Box<EvalAltResult>> {
|
||||
acc.name = name.to_string(); Ok(acc)
|
||||
});
|
||||
engine.register_fn("set_user_id", |mut acc: Account, user_id: INT| -> Result<Account, Box<EvalAltResult>> {
|
||||
acc.user_id = user_id as u32; Ok(acc)
|
||||
});
|
||||
engine.register_fn("set_description", |mut acc: Account, description: ImmutableString| -> Result<Account, Box<EvalAltResult>> {
|
||||
acc.description = description.to_string(); Ok(acc)
|
||||
});
|
||||
engine.register_fn("set_ledger", |mut acc: Account, ledger: ImmutableString| -> Result<Account, Box<EvalAltResult>> {
|
||||
acc.ledger = ledger.to_string(); Ok(acc)
|
||||
});
|
||||
engine.register_fn("set_address", |mut acc: Account, address: ImmutableString| -> Result<Account, Box<EvalAltResult>> {
|
||||
acc.address = address.to_string(); Ok(acc)
|
||||
});
|
||||
engine.register_fn("set_pubkey", |mut acc: Account, pubkey: ImmutableString| -> Result<Account, Box<EvalAltResult>> {
|
||||
acc.pubkey = pubkey.to_string(); Ok(acc)
|
||||
});
|
||||
// Action: Add an Asset object to the account's asset list
|
||||
engine.register_fn("add_asset", |mut acc: Account, asset: Asset| -> Result<Account, Box<EvalAltResult>> {
|
||||
acc.assets.push(asset);
|
||||
Ok(acc)
|
||||
});
|
||||
|
||||
// --- Asset model ---
|
||||
engine.register_type_with_name::<Asset>("Asset");
|
||||
engine.register_fn("new_asset", || -> Asset {
|
||||
Asset::new(None, "", "", 0.0, "", AssetType::Native, 0)
|
||||
});
|
||||
// Getters
|
||||
engine.register_get("id", |asset: &mut Asset| asset.base_data.id as INT);
|
||||
engine.register_get("created_at_ts", |asset: &mut Asset| asset.base_data.created_at);
|
||||
engine.register_get("name", |asset: &mut Asset| -> ImmutableString { asset.name.clone().into() });
|
||||
engine.register_get("description", |asset: &mut Asset| -> ImmutableString { asset.description.clone().into() });
|
||||
engine.register_get("amount", |asset: &mut Asset| asset.amount);
|
||||
engine.register_get("address", |asset: &mut Asset| -> ImmutableString { asset.address.clone().into() });
|
||||
engine.register_get("asset_type_str", |asset: &mut Asset| -> ImmutableString { self::asset_type_to_string(&asset.asset_type) });
|
||||
engine.register_get("decimals", |asset: &mut Asset| asset.decimals as INT);
|
||||
engine.register_get("modified_at_ts", |asset: &mut Asset| asset.base_data.modified_at);
|
||||
|
||||
// Setters (Builder Pattern)
|
||||
engine.register_fn("set_name", |mut asset: Asset, name: ImmutableString| -> Result<Asset, Box<EvalAltResult>> {
|
||||
asset.name = name.to_string(); Ok(asset)
|
||||
});
|
||||
engine.register_fn("set_description", |mut asset: Asset, description: ImmutableString| -> Result<Asset, Box<EvalAltResult>> {
|
||||
asset.description = description.to_string(); Ok(asset)
|
||||
});
|
||||
engine.register_fn("set_amount", |mut asset: Asset, amount: f64| -> Result<Asset, Box<EvalAltResult>> {
|
||||
asset.amount = amount; Ok(asset)
|
||||
});
|
||||
engine.register_fn("set_address", |mut asset: Asset, address: ImmutableString| -> Result<Asset, Box<EvalAltResult>> {
|
||||
asset.address = address.to_string(); Ok(asset)
|
||||
});
|
||||
engine.register_fn("set_asset_type", |mut asset: Asset, asset_type_str: ImmutableString| -> Result<Asset, Box<EvalAltResult>> {
|
||||
asset.asset_type = self::string_to_asset_type(asset_type_str.as_str())?;
|
||||
Ok(asset)
|
||||
});
|
||||
engine.register_fn("set_decimals", |mut asset: Asset, decimals: INT| -> Result<Asset, Box<EvalAltResult>> {
|
||||
asset.decimals = decimals as u8; Ok(asset)
|
||||
});
|
||||
|
||||
// --- Listing model ---
|
||||
engine.register_type_with_name::<Listing>("Listing");
|
||||
engine.register_fn("new_listing", || -> Listing {
|
||||
Listing::new(None, "", "", "", AssetType::Native, "", 0.0, "", ListingType::FixedPrice, None, Vec::new(), None::<String>)
|
||||
});
|
||||
// Getters
|
||||
engine.register_get("id", |l: &mut Listing| l.base_data.id as INT);
|
||||
engine.register_get("created_at_ts", |l: &mut Listing| l.base_data.created_at);
|
||||
engine.register_get("modified_at_ts", |l: &mut Listing| l.base_data.modified_at);
|
||||
engine.register_get("title", |l: &mut Listing| -> ImmutableString { l.title.clone().into() });
|
||||
engine.register_get("description", |l: &mut Listing| -> ImmutableString { l.description.clone().into() });
|
||||
engine.register_get("asset_id", |l: &mut Listing| -> ImmutableString { l.asset_id.clone().into() });
|
||||
engine.register_get("asset_type_str", |l: &mut Listing| -> ImmutableString { self::asset_type_to_string(&l.asset_type) });
|
||||
engine.register_get("seller_id", |l: &mut Listing| -> ImmutableString { l.seller_id.clone().into() });
|
||||
engine.register_get("price", |l: &mut Listing| l.price);
|
||||
engine.register_get("currency", |l: &mut Listing| -> ImmutableString { l.currency.clone().into() });
|
||||
engine.register_get("listing_type", |l: &mut Listing| l.listing_type.clone());
|
||||
engine.register_get("status", |l: &mut Listing| l.status.clone());
|
||||
engine.register_get("expires_at_ts", |l: &mut Listing| l.expires_at);
|
||||
engine.register_get("expires_at_ts_opt", |l: &mut Listing| l.expires_at.map(|dt| dt.timestamp()));
|
||||
engine.register_get("tags", |l: &mut Listing| -> Result<Array, Box<EvalAltResult>> {
|
||||
Ok(l.tags.iter().map(|s| Dynamic::from(s.clone())).collect())
|
||||
});
|
||||
engine.register_get("image_url", |l: &mut Listing| -> Option<ImmutableString> { l.image_url.as_ref().map(|s| s.clone().into()) });
|
||||
engine.register_get("bids_list", |l: &mut Listing| -> Result<Array, Box<EvalAltResult>> {
|
||||
Ok(l.bids.iter().cloned().map(rhai::Dynamic::from).collect())
|
||||
});
|
||||
// Setters (Builder Pattern)
|
||||
engine.register_fn("set_title", |mut l: Listing, title: ImmutableString| -> Result<Listing, Box<EvalAltResult>> {
|
||||
l.title = title.to_string(); Ok(l)
|
||||
});
|
||||
engine.register_fn("set_description", |mut l: Listing, description: ImmutableString| -> Result<Listing, Box<EvalAltResult>> {
|
||||
l.description = description.to_string(); Ok(l)
|
||||
});
|
||||
engine.register_fn("set_asset_id", |mut l: Listing, asset_id: ImmutableString| -> Result<Listing, Box<EvalAltResult>> {
|
||||
l.asset_id = asset_id.to_string(); Ok(l)
|
||||
});
|
||||
engine.register_fn("set_asset_type", |mut l: Listing, asset_type_str: ImmutableString| -> Result<Listing, Box<EvalAltResult>> {
|
||||
l.asset_type = self::string_to_asset_type(asset_type_str.as_str())?;
|
||||
Ok(l)
|
||||
});
|
||||
engine.register_fn("set_seller_id", |mut l: Listing, seller_id: ImmutableString| -> Result<Listing, Box<EvalAltResult>> {
|
||||
l.seller_id = seller_id.to_string(); Ok(l)
|
||||
});
|
||||
engine.register_fn("set_price", |mut l: Listing, price: f64| -> Result<Listing, Box<EvalAltResult>> {
|
||||
l.price = price; Ok(l)
|
||||
});
|
||||
engine.register_fn("set_currency", |mut l: Listing, currency: ImmutableString| -> Result<Listing, Box<EvalAltResult>> {
|
||||
l.currency = currency.to_string(); Ok(l)
|
||||
});
|
||||
engine.register_fn("set_listing_type", |mut l: Listing, listing_type: ImmutableString| -> Result<Listing, Box<EvalAltResult>> {
|
||||
l.listing_type = self::string_to_listing_type(listing_type.as_str())?;
|
||||
Ok(l)
|
||||
});
|
||||
engine.register_fn("set_status_str", |mut l: Listing, status_str: ImmutableString| -> Result<Listing, Box<EvalAltResult>> {
|
||||
l.status = self::string_to_listing_status(status_str.as_str())?;
|
||||
Ok(l)
|
||||
});
|
||||
engine.register_fn("set_expires_at_ts", |mut l: Listing, expires_at_ts: Option<INT>| -> Result<Listing, Box<EvalAltResult>> {
|
||||
l.expires_at = expires_at_ts.map(|ts| DateTime::from_timestamp(ts, 0).unwrap_or_else(|| Utc::now()));
|
||||
Ok(l)
|
||||
});
|
||||
engine.register_fn("set_tags", |mut l: Listing, tags_array: Array| -> Result<Listing, Box<EvalAltResult>> {
|
||||
l.tags = tags_array.into_iter().map(|d| d.into_string().unwrap_or_default()).collect();
|
||||
Ok(l)
|
||||
});
|
||||
engine.register_fn("add_tag", |mut l: Listing, tag: ImmutableString| -> Result<Listing, Box<EvalAltResult>> {
|
||||
l.tags.push(tag.to_string());
|
||||
Ok(l)
|
||||
});
|
||||
engine.register_fn("set_image_url", |mut l: Listing, image_url: Option<ImmutableString>| -> Result<Listing, Box<EvalAltResult>> {
|
||||
l.image_url = image_url.map(|s| s.to_string());
|
||||
Ok(l)
|
||||
});
|
||||
// Listing Action Methods (preserved)
|
||||
engine.register_fn("add_listing_bid", |listing: Listing, bid: Bid| -> Result<Listing, Box<EvalAltResult>> {
|
||||
listing.add_bid(bid).map_err(|e_str| {
|
||||
Box::new(EvalAltResult::ErrorSystem(
|
||||
"Failed to add bid".to_string(),
|
||||
Box::new(RhaiStringError(e_str.to_string())),
|
||||
))
|
||||
})
|
||||
});
|
||||
engine.register_fn("accept_listing_bid", |listing: Listing, bid_index_rhai: i64| -> Result<Listing, Box<EvalAltResult>> {
|
||||
let bid_index = bid_index_rhai as usize;
|
||||
if bid_index >= listing.bids.len() {
|
||||
return Err(Box::new(EvalAltResult::ErrorSystem(
|
||||
"Invalid bid index".to_string(),
|
||||
Box::new(RhaiStringError(format!("Bid index {} out of bounds for {} bids", bid_index, listing.bids.len()))),
|
||||
)));
|
||||
}
|
||||
|
||||
let bid_to_accept = listing.bids[bid_index].clone();
|
||||
|
||||
if bid_to_accept.status != BidStatus::Active {
|
||||
return Err(Box::new(EvalAltResult::ErrorSystem(
|
||||
"Bid not active".to_string(),
|
||||
Box::new(RhaiStringError(format!("Cannot accept bid at index {} as it is not active (status: {:?})", bid_index, bid_to_accept.status))),
|
||||
)));
|
||||
}
|
||||
|
||||
let mut listing_after_sale = listing.complete_sale(bid_to_accept.bidder_id.to_string(), bid_to_accept.amount)
|
||||
.map_err(|e_str| Box::new(EvalAltResult::ErrorSystem(
|
||||
"Failed to complete sale".to_string(),
|
||||
Box::new(RhaiStringError(e_str.to_string())),
|
||||
)))?;
|
||||
|
||||
// Update bid statuses on the new listing state
|
||||
for (idx, bid_in_list) in listing_after_sale.bids.iter_mut().enumerate() {
|
||||
if idx == bid_index {
|
||||
*bid_in_list = bid_in_list.clone().update_status(BidStatus::Accepted);
|
||||
} else {
|
||||
if bid_in_list.status == BidStatus::Active { // Only reject other active bids
|
||||
*bid_in_list = bid_in_list.clone().update_status(BidStatus::Rejected);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(listing_after_sale)
|
||||
});
|
||||
engine.register_fn("cancel_listing", |_ctx: NativeCallContext, listing: Listing| -> Result<Listing, Box<EvalAltResult>> {
|
||||
listing.cancel().map_err(|e_str| {
|
||||
Box::new(EvalAltResult::ErrorSystem(
|
||||
"Failed to cancel listing".to_string(),
|
||||
Box::new(RhaiStringError(e_str.to_string()))
|
||||
))
|
||||
})
|
||||
});
|
||||
// --- Bid model (preserved as is) ---
|
||||
engine.register_type_with_name::<Bid>("Bid")
|
||||
.register_fn("new_bid",
|
||||
|listing_id_rhai: ImmutableString, bidder_id_rhai: INT, amount_rhai: f64, currency_rhai: ImmutableString| -> Bid {
|
||||
Bid::new(listing_id_rhai, bidder_id_rhai as u32, amount_rhai, currency_rhai)
|
||||
}
|
||||
)
|
||||
.register_get_set("listing_id",
|
||||
|bid: &mut Bid| -> ImmutableString { bid.listing_id.clone().into() },
|
||||
|bid: &mut Bid, val: ImmutableString| bid.listing_id = val.to_string()
|
||||
)
|
||||
.register_get_set("bidder_id", |bid: &mut Bid| bid.bidder_id as INT, |bid: &mut Bid, val: INT| bid.bidder_id = val as u32)
|
||||
.register_get_set("amount", |bid: &mut Bid| bid.amount, |bid: &mut Bid, val: f64| bid.amount = val)
|
||||
.register_get_set("currency",
|
||||
|bid: &mut Bid| -> ImmutableString { bid.currency.clone().into() },
|
||||
|bid: &mut Bid, val: ImmutableString| bid.currency = val.to_string()
|
||||
)
|
||||
.register_get("status_str", |bid: &mut Bid| -> ImmutableString { self::bid_status_to_string(&bid.status) });
|
||||
|
||||
engine.register_fn("accept_bid_script", |bid: Bid| -> Result<Bid, Box<EvalAltResult>> {
|
||||
// Ensure the bid is active before accepting
|
||||
if bid.status != BidStatus::Active {
|
||||
return Err(Box::new(EvalAltResult::ErrorSystem(
|
||||
"Bid not active".to_string(),
|
||||
Box::new(RhaiStringError(format!("Cannot accept bid as it is not active (status: {:?})", bid.status)))
|
||||
)));
|
||||
}
|
||||
Ok(bid.update_status(BidStatus::Accepted))
|
||||
});
|
||||
engine.register_fn("reject_bid_script", |bid: Bid| -> Result<Bid, Box<EvalAltResult>> {
|
||||
// Ensure the bid is active before rejecting
|
||||
if bid.status != BidStatus::Active {
|
||||
return Err(Box::new(EvalAltResult::ErrorSystem(
|
||||
"Bid not active".to_string(),
|
||||
Box::new(RhaiStringError(format!("Cannot reject bid as it is not active (status: {:?})", bid.status)))
|
||||
)));
|
||||
}
|
||||
Ok(bid.update_status(BidStatus::Rejected))
|
||||
});
|
||||
engine.register_fn("cancel_bid_script", |bid: Bid| -> Result<Bid, Box<EvalAltResult>> {
|
||||
// Ensure the bid is active before cancelling
|
||||
if bid.status != BidStatus::Active {
|
||||
return Err(Box::new(EvalAltResult::ErrorSystem(
|
||||
"Bid not active".to_string(),
|
||||
Box::new(RhaiStringError(format!("Cannot cancel bid as it is not active (status: {:?})", bid.status)))
|
||||
)));
|
||||
}
|
||||
Ok(bid.update_status(BidStatus::Cancelled))
|
||||
});
|
||||
|
||||
// --- Global Helper Functions (Enum conversions, potentially already covered by macros but good for direct script use) ---
|
||||
engine.register_fn("str_to_asset_type", |s: ImmutableString| self::string_to_asset_type(s.as_str()));
|
||||
engine.register_fn("asset_type_to_str", self::asset_type_to_string);
|
||||
engine.register_fn("str_to_listing_status", |s: ImmutableString| self::string_to_listing_status(s.as_str()));
|
||||
engine.register_fn("listing_status_to_str", self::listing_status_to_string);
|
||||
engine.register_fn("str_to_listing_type", |s: ImmutableString| self::string_to_listing_type(s.as_str()));
|
||||
engine.register_fn("listing_type_to_str", self::listing_type_to_string);
|
||||
engine.register_fn("str_to_bid_status", |s: ImmutableString| self::string_to_bid_status(s.as_str()));
|
||||
engine.register_fn("bid_status_to_str", self::bid_status_to_string);
|
||||
|
||||
// --- Mock DB functions (preserved) ---
|
||||
let accounts_db_clone = Arc::clone(&db_accounts);
|
||||
engine.register_fn("set_account", move |mut account: Account| -> Account {
|
||||
let mut db = accounts_db_clone.lock().unwrap();
|
||||
if account.base_data.id == 0 {
|
||||
let next_id = db.keys().max().cloned().unwrap_or(0) + 1;
|
||||
account.base_data.update_id(next_id);
|
||||
}
|
||||
db.insert(account.base_data.id, account.clone());
|
||||
account
|
||||
});
|
||||
|
||||
let accounts_db_clone_get = Arc::clone(&db_accounts);
|
||||
engine.register_fn("get_account_by_id", move |id_rhai: INT| -> Result<Account, Box<EvalAltResult>> {
|
||||
let db = accounts_db_clone_get.lock().unwrap();
|
||||
match db.get(&(id_rhai as u32)) {
|
||||
Some(account) => Ok(account.clone()),
|
||||
None => Err(format!("Account not found with ID: {}", id_rhai).into()),
|
||||
}
|
||||
});
|
||||
|
||||
let assets_db_clone = Arc::clone(&db_assets);
|
||||
engine.register_fn("set_asset", move |mut asset: Asset| -> Asset {
|
||||
let mut db = assets_db_clone.lock().unwrap();
|
||||
if asset.base_data.id == 0 {
|
||||
let next_id = db.keys().max().cloned().unwrap_or(0) + 1;
|
||||
asset.base_data.update_id(next_id);
|
||||
}
|
||||
db.insert(asset.base_data.id, asset.clone());
|
||||
asset
|
||||
});
|
||||
|
||||
let assets_db_clone_get = Arc::clone(&db_assets);
|
||||
engine.register_fn("get_asset_by_id", move |id_rhai: INT| -> Result<Asset, Box<EvalAltResult>> {
|
||||
let db = assets_db_clone_get.lock().unwrap();
|
||||
match db.get(&(id_rhai as u32)) {
|
||||
Some(asset) => Ok(asset.clone()),
|
||||
None => Err(format!("Asset not found with ID: {}", id_rhai).into()),
|
||||
}
|
||||
});
|
||||
|
||||
let listings_db_clone = Arc::clone(&db_listings);
|
||||
engine.register_fn("set_listing", move |mut listing: Listing| -> Listing {
|
||||
let mut db = listings_db_clone.lock().unwrap();
|
||||
if listing.base_data.id == 0 {
|
||||
let next_id = db.keys().max().cloned().unwrap_or(0) + 1;
|
||||
listing.base_data.update_id(next_id);
|
||||
}
|
||||
db.insert(listing.base_data.id, listing.clone());
|
||||
listing
|
||||
});
|
||||
|
||||
let listings_db_clone_get = Arc::clone(&db_listings);
|
||||
engine.register_fn("get_listing_by_id", move |id_rhai: INT| -> Result<Listing, Box<EvalAltResult>> {
|
||||
let db = listings_db_clone_get.lock().unwrap();
|
||||
match db.get(&(id_rhai as u32)) {
|
||||
Some(listing) => Ok(listing.clone()),
|
||||
None => Err(format!("Listing not found with ID: {}", id_rhai).into()),
|
||||
}
|
||||
});
|
||||
|
||||
// Global timestamp function for scripts to get current time
|
||||
engine.register_fn("timestamp", || Utc::now().timestamp());
|
||||
}
|
56
heromodels/src/models/flow/flow.rs
Normal file
56
heromodels/src/models/flow/flow.rs
Normal file
@@ -0,0 +1,56 @@
|
||||
use heromodels_core::BaseModelData;
|
||||
use heromodels_derive::model;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use super::flow_step::FlowStep;
|
||||
|
||||
/// Represents a signing flow.
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
#[model]
|
||||
pub struct Flow {
|
||||
/// Base model data (id, created_at, updated_at).
|
||||
pub base_data: BaseModelData,
|
||||
|
||||
/// A unique UUID for the flow, for external reference.
|
||||
#[index]
|
||||
pub flow_uuid: String,
|
||||
|
||||
/// Name of the flow.
|
||||
#[index]
|
||||
pub name: String,
|
||||
|
||||
/// Current status of the flow (e.g., "Pending", "InProgress", "Completed", "Failed").
|
||||
pub status: String,
|
||||
|
||||
/// Steps involved in this flow.
|
||||
pub steps: Vec<FlowStep>,
|
||||
}
|
||||
|
||||
impl Flow {
|
||||
/// Create a new flow.
|
||||
/// The `id` is the database primary key.
|
||||
/// The `flow_uuid` should be a Uuid::new_v4().to_string().
|
||||
pub fn new(_id: u32, flow_uuid: impl ToString) -> Self {
|
||||
Self {
|
||||
base_data: BaseModelData::new(),
|
||||
flow_uuid: flow_uuid.to_string(),
|
||||
name: String::new(), // Default name, to be set by builder
|
||||
status: String::from("Pending"), // Default status, to be set by builder
|
||||
steps: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(mut self, name: impl ToString) -> Self {
|
||||
self.name = name.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn status(mut self, status: impl ToString) -> Self {
|
||||
self.status = status.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_step(mut self, step: FlowStep) -> Self {
|
||||
self.steps.push(step);
|
||||
self
|
||||
}
|
||||
}
|
44
heromodels/src/models/flow/flow_step.rs
Normal file
44
heromodels/src/models/flow/flow_step.rs
Normal file
@@ -0,0 +1,44 @@
|
||||
use heromodels_core::BaseModelData;
|
||||
use heromodels_derive::model;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Represents a step within a signing flow.
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
#[model]
|
||||
pub struct FlowStep {
|
||||
/// Base model data.
|
||||
pub base_data: BaseModelData,
|
||||
|
||||
/// Optional description for the step.
|
||||
pub description: Option<String>,
|
||||
|
||||
/// Order of this step within the flow.
|
||||
#[index]
|
||||
pub step_order: u32,
|
||||
|
||||
/// Current status of the flow step (e.g., "Pending", "InProgress", "Completed", "Failed").
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
impl FlowStep {
|
||||
/// Create a new flow step.
|
||||
pub fn new(_id: u32, step_order: u32) -> Self {
|
||||
Self {
|
||||
base_data: BaseModelData::new(),
|
||||
description: None,
|
||||
step_order,
|
||||
status: String::from("Pending"), // Default status
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the description for the flow step.
|
||||
pub fn description(mut self, description: impl ToString) -> Self {
|
||||
self.description = Some(description.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn status(mut self, status: impl ToString) -> Self {
|
||||
self.status = status.to_string();
|
||||
self
|
||||
}
|
||||
}
|
11
heromodels/src/models/flow/mod.rs
Normal file
11
heromodels/src/models/flow/mod.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
// Export flow model submodules
|
||||
pub mod flow;
|
||||
pub mod flow_step;
|
||||
pub mod signature_requirement;
|
||||
pub mod rhai;
|
||||
|
||||
// Re-export key types for convenience
|
||||
pub use flow::Flow;
|
||||
pub use flow_step::FlowStep;
|
||||
pub use signature_requirement::SignatureRequirement;
|
||||
pub use rhai::register_flow_rhai_module;
|
140
heromodels/src/models/flow/rhai.rs
Normal file
140
heromodels/src/models/flow/rhai.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
use rhai::{Dynamic, Engine, EvalAltResult, NativeCallContext, Position};
|
||||
use std::sync::Arc;
|
||||
|
||||
use heromodels_core::BaseModelData;
|
||||
use crate::db::hero::OurDB; // Import OurDB for actual DB operations
|
||||
use crate::db::Collection; // Collection might be needed if we add more specific DB functions
|
||||
use super::{
|
||||
flow::Flow,
|
||||
flow_step::FlowStep,
|
||||
signature_requirement::SignatureRequirement,
|
||||
};
|
||||
// use rhai_wrapper::wrap_vec_return; // Not currently used for flow, but keep for potential future use.
|
||||
|
||||
// Helper function to convert Rhai's i64 to u32 for IDs
|
||||
fn i64_to_u32(val: i64, context_pos: Position, field_name: &str, object_name: &str) -> Result<u32, Box<EvalAltResult>> {
|
||||
val.try_into().map_err(|_e| {
|
||||
Box::new(EvalAltResult::ErrorArithmetic(
|
||||
format!("Conversion error for {} in {} from i64 to u32", field_name, object_name),
|
||||
context_pos,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn register_flow_rhai_module(engine: &mut Engine, db: Arc<OurDB>) {
|
||||
// --- Flow Model ---
|
||||
|
||||
// Constructor: new_flow(id: u32, flow_uuid: String)
|
||||
engine.register_fn("new_flow", move |context: NativeCallContext, id_i64: i64, flow_uuid: String| -> Result<Flow, Box<EvalAltResult>> {
|
||||
let id_u32 = i64_to_u32(id_i64, context.position(), "id", "new_flow")?;
|
||||
Ok(Flow::new(id_u32, flow_uuid))
|
||||
});
|
||||
|
||||
// Builder methods for Flow
|
||||
engine.register_fn("name", |flow: Flow, name_val: String| -> Flow { flow.name(name_val) });
|
||||
engine.register_fn("status", |flow: Flow, status_val: String| -> Flow { flow.status(status_val) });
|
||||
engine.register_fn("add_step", |flow: Flow, step: FlowStep| -> Flow { flow.add_step(step) });
|
||||
|
||||
// Getters for Flow fields
|
||||
engine.register_get("id", |flow: &mut Flow| -> Result<i64, Box<EvalAltResult>> { Ok(flow.base_data.id as i64) });
|
||||
engine.register_get("base_data", |flow: &mut Flow| -> Result<BaseModelData, Box<EvalAltResult>> { Ok(flow.base_data.clone()) });
|
||||
engine.register_get("flow_uuid", |flow: &mut Flow| -> Result<String, Box<EvalAltResult>> { Ok(flow.flow_uuid.clone()) });
|
||||
engine.register_get("name", |flow: &mut Flow| -> Result<String, Box<EvalAltResult>> { Ok(flow.name.clone()) });
|
||||
engine.register_get("status", |flow: &mut Flow| -> Result<String, Box<EvalAltResult>> { Ok(flow.status.clone()) });
|
||||
engine.register_get("steps", |flow: &mut Flow| -> Result<rhai::Array, Box<EvalAltResult>> {
|
||||
let rhai_array = flow.steps.iter().cloned().map(Dynamic::from).collect::<rhai::Array>();
|
||||
Ok(rhai_array)
|
||||
});
|
||||
|
||||
// --- FlowStep Model ---
|
||||
|
||||
// Constructor: new_flow_step(id: u32, step_order: u32)
|
||||
engine.register_fn("new_flow_step", move |context: NativeCallContext, id_i64: i64, step_order_i64: i64| -> Result<FlowStep, Box<EvalAltResult>> {
|
||||
let id_u32 = i64_to_u32(id_i64, context.position(), "id", "new_flow_step")?;
|
||||
let step_order_u32 = i64_to_u32(step_order_i64, context.position(), "step_order", "new_flow_step")?;
|
||||
Ok(FlowStep::new(id_u32, step_order_u32))
|
||||
});
|
||||
|
||||
// Builder methods for FlowStep
|
||||
engine.register_fn("description", |fs: FlowStep, desc_val: String| -> FlowStep { fs.description(desc_val) }); // Assuming FlowStep::description takes impl ToString
|
||||
engine.register_fn("status", |fs: FlowStep, status_val: String| -> FlowStep { fs.status(status_val) });
|
||||
|
||||
// Getters for FlowStep fields
|
||||
engine.register_get("id", |step: &mut FlowStep| -> Result<i64, Box<EvalAltResult>> { Ok(step.base_data.id as i64) });
|
||||
engine.register_get("base_data", |step: &mut FlowStep| -> Result<BaseModelData, Box<EvalAltResult>> { Ok(step.base_data.clone()) });
|
||||
engine.register_get("description", |step: &mut FlowStep| -> Result<Dynamic, Box<EvalAltResult>> { Ok(match step.description.clone() { Some(s) => Dynamic::from(s), None => Dynamic::from(()) }) });
|
||||
engine.register_get("step_order", |step: &mut FlowStep| -> Result<i64, Box<EvalAltResult>> { Ok(step.step_order as i64) });
|
||||
engine.register_get("status", |step: &mut FlowStep| -> Result<String, Box<EvalAltResult>> { Ok(step.status.clone()) });
|
||||
|
||||
// --- SignatureRequirement Model ---
|
||||
|
||||
// Constructor: new_signature_requirement(id: u32, flow_step_id: u32, public_key: String, message: String)
|
||||
engine.register_fn("new_signature_requirement",
|
||||
move |context: NativeCallContext, id_i64: i64, flow_step_id_i64: i64, public_key: String, message: String|
|
||||
-> Result<SignatureRequirement, Box<EvalAltResult>> {
|
||||
let id_u32 = i64_to_u32(id_i64, context.position(), "id", "new_signature_requirement")?;
|
||||
let flow_step_id_u32 = i64_to_u32(flow_step_id_i64, context.position(), "flow_step_id", "new_signature_requirement")?;
|
||||
Ok(SignatureRequirement::new(id_u32, flow_step_id_u32, public_key, message))
|
||||
});
|
||||
|
||||
// Builder methods for SignatureRequirement
|
||||
engine.register_fn("signed_by", |sr: SignatureRequirement, signed_by_val: String| -> SignatureRequirement { sr.signed_by(signed_by_val) }); // Assuming SR::signed_by takes impl ToString
|
||||
engine.register_fn("signature", |sr: SignatureRequirement, sig_val: String| -> SignatureRequirement { sr.signature(sig_val) }); // Assuming SR::signature takes impl ToString
|
||||
engine.register_fn("status", |sr: SignatureRequirement, status_val: String| -> SignatureRequirement { sr.status(status_val) });
|
||||
|
||||
// Getters for SignatureRequirement fields
|
||||
engine.register_get("id", |sr: &mut SignatureRequirement| -> Result<i64, Box<EvalAltResult>> { Ok(sr.base_data.id as i64) });
|
||||
engine.register_get("base_data", |sr: &mut SignatureRequirement| -> Result<BaseModelData, Box<EvalAltResult>> { Ok(sr.base_data.clone()) });
|
||||
engine.register_get("flow_step_id", |sr: &mut SignatureRequirement| -> Result<i64, Box<EvalAltResult>> { Ok(sr.flow_step_id as i64) });
|
||||
engine.register_get("public_key", |sr: &mut SignatureRequirement| -> Result<String, Box<EvalAltResult>> { Ok(sr.public_key.clone()) });
|
||||
engine.register_get("message", |sr: &mut SignatureRequirement| -> Result<String, Box<EvalAltResult>> { Ok(sr.message.clone()) });
|
||||
engine.register_get("signed_by", |sr: &mut SignatureRequirement| -> Result<Dynamic, Box<EvalAltResult>> { Ok(match sr.signed_by.clone() { Some(s) => Dynamic::from(s), None => Dynamic::from(()) }) });
|
||||
engine.register_get("signature", |sr: &mut SignatureRequirement| -> Result<Dynamic, Box<EvalAltResult>> { Ok(match sr.signature.clone() { Some(s) => Dynamic::from(s), None => Dynamic::from(()) }) });
|
||||
engine.register_get("status", |sr: &mut SignatureRequirement| -> Result<String, Box<EvalAltResult>> { Ok(sr.status.clone()) });
|
||||
|
||||
// --- BaseModelData Getters (if not already globally registered) ---
|
||||
// Assuming these might be specific to the context or shadowed, explicit registration is safer.
|
||||
engine.register_get("id", |bmd: &mut BaseModelData| -> Result<i64, Box<EvalAltResult>> { Ok(bmd.id as i64) });
|
||||
engine.register_get("created_at", |bmd: &mut BaseModelData| -> Result<i64, Box<EvalAltResult>> { Ok(bmd.created_at) });
|
||||
engine.register_get("modified_at", |bmd: &mut BaseModelData| -> Result<i64, Box<EvalAltResult>> { Ok(bmd.modified_at) });
|
||||
// engine.register_get("comments", |bmd: &mut BaseModelData| Ok(bmd.comments.clone())); // Rhai might need specific handling for Vec<String>
|
||||
|
||||
// --- Database Interaction Functions ---
|
||||
|
||||
let captured_db_for_set_flow = Arc::clone(&db);
|
||||
engine.register_fn("set_flow", move |flow: Flow| -> Result<(), Box<EvalAltResult>> {
|
||||
captured_db_for_set_flow.set(&flow).map(|_| ()).map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set Flow (ID: {}): {}", flow.base_data.id, e).into(), Position::NONE))
|
||||
})
|
||||
});
|
||||
|
||||
let captured_db_for_get_flow = Arc::clone(&db);
|
||||
engine.register_fn("get_flow_by_id", move |context: NativeCallContext, id_i64: i64| -> Result<Flow, Box<EvalAltResult>> {
|
||||
let id_u32 = i64_to_u32(id_i64, context.position(), "id", "get_flow_by_id")?;
|
||||
captured_db_for_get_flow.get_by_id(id_u32)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting Flow (ID: {}): {}", id_u32, e).into(), Position::NONE)))?
|
||||
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Flow with ID {} not found", id_u32).into(), Position::NONE)))
|
||||
});
|
||||
// Add get_flows_by_uuid, flow_exists etc. as needed, using wrap_vec_return for Vec results.
|
||||
|
||||
// FlowStep DB functions are removed as FlowSteps are now part of Flow.
|
||||
|
||||
let captured_db_for_set_sig_req = Arc::clone(&db);
|
||||
engine.register_fn("set_signature_requirement", move |sr: SignatureRequirement| -> Result<(), Box<EvalAltResult>> {
|
||||
captured_db_for_set_sig_req.set(&sr).map(|_| ()).map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set SignatureRequirement (ID: {}): {}", sr.base_data.id, e).into(), Position::NONE))
|
||||
})
|
||||
});
|
||||
|
||||
let captured_db_for_get_sig_req = Arc::clone(&db);
|
||||
engine.register_fn("get_signature_requirement_by_id",
|
||||
move |context: NativeCallContext, id_i64: i64|
|
||||
-> Result<SignatureRequirement, Box<EvalAltResult>> {
|
||||
let id_u32 = i64_to_u32(id_i64, context.position(), "id", "get_signature_requirement_by_id")?;
|
||||
captured_db_for_get_sig_req.get_by_id(id_u32)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting SignatureRequirement (ID: {}): {}", id_u32, e).into(), Position::NONE)))?
|
||||
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("SignatureRequirement with ID {} not found", id_u32).into(), Position::NONE)))
|
||||
});
|
||||
|
||||
println!("Flow Rhai module registered.");
|
||||
}
|
62
heromodels/src/models/flow/signature_requirement.rs
Normal file
62
heromodels/src/models/flow/signature_requirement.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use heromodels_core::BaseModelData;
|
||||
use heromodels_derive::model;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Represents a signature requirement for a flow step.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[model]
|
||||
pub struct SignatureRequirement {
|
||||
/// Base model data.
|
||||
pub base_data: BaseModelData,
|
||||
|
||||
/// Foreign key to the FlowStep this requirement belongs to.
|
||||
#[index]
|
||||
pub flow_step_id: u32,
|
||||
|
||||
/// The public key required to sign the message.
|
||||
pub public_key: String,
|
||||
|
||||
/// The plaintext message to be signed.
|
||||
pub message: String,
|
||||
|
||||
/// The public key of the entity that signed the message, if signed.
|
||||
pub signed_by: Option<String>,
|
||||
|
||||
/// The signature, if signed.
|
||||
pub signature: Option<String>,
|
||||
|
||||
/// Current status of the signature requirement (e.g., "Pending", "SentToClient", "Signed", "Failed", "Error").
|
||||
pub status: String,
|
||||
}
|
||||
|
||||
impl SignatureRequirement {
|
||||
/// Create a new signature requirement.
|
||||
pub fn new(_id: u32, flow_step_id: u32, public_key: impl ToString, message: impl ToString) -> Self {
|
||||
Self {
|
||||
base_data: BaseModelData::new(),
|
||||
flow_step_id,
|
||||
public_key: public_key.to_string(),
|
||||
message: message.to_string(),
|
||||
signed_by: None,
|
||||
signature: None,
|
||||
status: String::from("Pending"), // Default status
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the public key of the signer.
|
||||
pub fn signed_by(mut self, signed_by: impl ToString) -> Self {
|
||||
self.signed_by = Some(signed_by.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the signature.
|
||||
pub fn signature(mut self, signature: impl ToString) -> Self {
|
||||
self.signature = Some(signature.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn status(mut self, status: impl ToString) -> Self {
|
||||
self.status = status.to_string();
|
||||
self
|
||||
}
|
||||
}
|
7
heromodels/src/models/governance/mod.rs
Normal file
7
heromodels/src/models/governance/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
// heromodels/src/models/governance/mod.rs
|
||||
// This module will contain the Proposal model and related types.
|
||||
pub mod proposal;
|
||||
|
||||
pub use self::proposal::{
|
||||
Activity, ActivityType, Ballot, Proposal, ProposalStatus, VoteEventStatus, VoteOption,
|
||||
};
|
434
heromodels/src/models/governance/proposal.rs
Normal file
434
heromodels/src/models/governance/proposal.rs
Normal file
@@ -0,0 +1,434 @@
|
||||
// heromodels/src/models/governance/proposal.rs
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use heromodels_derive::model; // For #[model]
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use rhai_autobind_macros::rhai_model_export;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use heromodels_core::BaseModelData;
|
||||
|
||||
// --- Enums ---
|
||||
|
||||
/// ProposalStatus defines the lifecycle status of a governance proposal itself
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ProposalStatus {
|
||||
Draft, // Proposal is being prepared
|
||||
Active, // Proposal is active
|
||||
Approved, // Proposal has been formally approved
|
||||
Rejected, // Proposal has been formally rejected
|
||||
Cancelled, // Proposal was cancelled
|
||||
}
|
||||
|
||||
impl Default for ProposalStatus {
|
||||
fn default() -> Self {
|
||||
ProposalStatus::Draft
|
||||
}
|
||||
}
|
||||
|
||||
/// VoteEventStatus represents the status of the voting process for a proposal
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum VoteEventStatus {
|
||||
Open, // Voting is currently open
|
||||
Closed, // Voting has finished
|
||||
Cancelled, // The voting event was cancelled
|
||||
}
|
||||
|
||||
impl Default for VoteEventStatus {
|
||||
fn default() -> Self {
|
||||
VoteEventStatus::Open
|
||||
}
|
||||
}
|
||||
|
||||
// --- Structs ---
|
||||
|
||||
/// VoteOption represents a specific choice that can be voted on
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
|
||||
pub struct VoteOption {
|
||||
pub id: u8, // Simple identifier for this option
|
||||
pub text: String, // Descriptive text of the option
|
||||
pub count: i64, // How many votes this option has received
|
||||
pub min_valid: Option<i64>, // Optional: minimum votes needed,
|
||||
pub comment: Option<String>, // Optional: comment
|
||||
}
|
||||
|
||||
impl VoteOption {
|
||||
pub fn new(id: u8, text: impl ToString, comment: Option<impl ToString>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
text: text.to_string(),
|
||||
count: 0,
|
||||
min_valid: None,
|
||||
comment: comment.map(|c| c.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ballot represents an individual vote cast by a user
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
|
||||
#[rhai_model_export(db_type = "std::sync::Arc<crate::db::hero::OurDB>")]
|
||||
#[model] // Has base.Base in V spec
|
||||
pub struct Ballot {
|
||||
pub base_data: BaseModelData,
|
||||
pub user_id: u32, // The ID of the user who cast this ballot
|
||||
pub vote_option_id: u8, // The 'id' of the VoteOption chosen
|
||||
pub shares_count: i64, // Number of shares/tokens/voting power
|
||||
pub comment: Option<String>, // Optional comment from the voter
|
||||
}
|
||||
|
||||
impl Ballot {
|
||||
/// Create a new ballot with auto-generated ID
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - Optional ID for the ballot (use None for auto-generated ID)
|
||||
/// * `user_id` - ID of the user who cast this ballot
|
||||
/// * `vote_option_id` - ID of the vote option chosen
|
||||
/// * `shares_count` - Number of shares/tokens/voting power
|
||||
pub fn new(id: Option<u32>, user_id: u32, vote_option_id: u8, shares_count: i64) -> Self {
|
||||
let mut base_data = BaseModelData::new();
|
||||
if let Some(id) = id {
|
||||
base_data.update_id(id);
|
||||
}
|
||||
|
||||
Self {
|
||||
base_data,
|
||||
user_id,
|
||||
vote_option_id,
|
||||
shares_count,
|
||||
comment: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Proposal represents a governance proposal that can be voted upon.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
|
||||
#[rhai_model_export(db_type = "std::sync::Arc<crate::db::hero::OurDB>")]
|
||||
#[model] // Has base.Base in V spec
|
||||
pub struct Proposal {
|
||||
pub base_data: BaseModelData,
|
||||
pub creator_id: String, // User ID of the proposal creator
|
||||
pub creator_name: String, // User name of the proposal creator
|
||||
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub status: ProposalStatus,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
|
||||
// Voting event aspects
|
||||
pub vote_start_date: DateTime<Utc>,
|
||||
pub vote_end_date: DateTime<Utc>,
|
||||
pub vote_status: VoteEventStatus,
|
||||
pub options: Vec<VoteOption>,
|
||||
pub ballots: Vec<Ballot>, // This will store actual Ballot structs
|
||||
pub private_group: Option<Vec<u32>>, // Optional list of eligible user IDs
|
||||
}
|
||||
|
||||
impl Proposal {
|
||||
/// Create a new proposal with auto-generated ID
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - Optional ID for the proposal (use None for auto-generated ID)
|
||||
/// * `creator_id` - ID of the user who created the proposal
|
||||
/// * `title` - Title of the proposal
|
||||
/// * `description` - Description of the proposal
|
||||
/// * `vote_start_date` - Date when voting starts
|
||||
/// * `vote_end_date` - Date when voting ends
|
||||
pub fn new(
|
||||
id: Option<u32>,
|
||||
creator_id: impl ToString,
|
||||
creator_name: impl ToString,
|
||||
title: impl ToString,
|
||||
description: impl ToString,
|
||||
status: ProposalStatus,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
vote_start_date: DateTime<Utc>,
|
||||
vote_end_date: DateTime<Utc>,
|
||||
) -> Self {
|
||||
let mut base_data = BaseModelData::new();
|
||||
if let Some(id) = id {
|
||||
base_data.update_id(id);
|
||||
}
|
||||
|
||||
Self {
|
||||
base_data,
|
||||
creator_id: creator_id.to_string(),
|
||||
creator_name: creator_name.to_string(),
|
||||
title: title.to_string(),
|
||||
description: description.to_string(),
|
||||
status,
|
||||
created_at,
|
||||
updated_at,
|
||||
vote_start_date,
|
||||
vote_end_date,
|
||||
vote_status: VoteEventStatus::Open, // Default to open when created
|
||||
options: Vec::new(),
|
||||
ballots: Vec::new(),
|
||||
private_group: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_option(
|
||||
mut self,
|
||||
option_id: u8,
|
||||
option_text: impl ToString,
|
||||
comment: Option<impl ToString>,
|
||||
) -> Self {
|
||||
let new_option = VoteOption::new(option_id, option_text, comment);
|
||||
self.options.push(new_option);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn cast_vote(
|
||||
mut self,
|
||||
ballot_id: Option<u32>,
|
||||
user_id: u32,
|
||||
chosen_option_id: u8,
|
||||
shares: i64,
|
||||
) -> Self {
|
||||
if self.vote_status != VoteEventStatus::Open {
|
||||
eprintln!("Voting is not open for proposal '{}'", self.title);
|
||||
return self;
|
||||
}
|
||||
if !self.options.iter().any(|opt| opt.id == chosen_option_id) {
|
||||
eprintln!(
|
||||
"Chosen option ID {} does not exist for proposal '{}'",
|
||||
chosen_option_id, self.title
|
||||
);
|
||||
return self;
|
||||
}
|
||||
if let Some(group) = &self.private_group {
|
||||
if !group.contains(&user_id) {
|
||||
eprintln!(
|
||||
"User {} is not eligible to vote on proposal '{}'",
|
||||
user_id, self.title
|
||||
);
|
||||
return self;
|
||||
}
|
||||
}
|
||||
|
||||
let new_ballot = Ballot::new(ballot_id, user_id, chosen_option_id, shares);
|
||||
self.ballots.push(new_ballot);
|
||||
|
||||
if let Some(option) = self
|
||||
.options
|
||||
.iter_mut()
|
||||
.find(|opt| opt.id == chosen_option_id)
|
||||
{
|
||||
option.count += shares;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn change_proposal_status(mut self, new_status: ProposalStatus) -> Self {
|
||||
self.status = new_status;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn change_vote_event_status(mut self, new_status: VoteEventStatus) -> Self {
|
||||
self.vote_status = new_status;
|
||||
self
|
||||
}
|
||||
|
||||
/// Cast a vote with a comment
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `ballot_id` - Optional ID for the ballot (use None for auto-generated ID)
|
||||
/// * `user_id` - ID of the user who is casting the vote
|
||||
/// * `chosen_option_id` - ID of the vote option chosen
|
||||
/// * `shares` - Number of shares/tokens/voting power
|
||||
/// * `comment` - Comment from the voter explaining their vote
|
||||
pub fn cast_vote_with_comment(
|
||||
mut self,
|
||||
ballot_id: Option<u32>,
|
||||
user_id: u32,
|
||||
chosen_option_id: u8,
|
||||
shares: i64,
|
||||
comment: impl ToString,
|
||||
) -> Self {
|
||||
// First check if voting is open
|
||||
if self.vote_status != VoteEventStatus::Open {
|
||||
eprintln!("Voting is not open for proposal '{}'", self.title);
|
||||
return self;
|
||||
}
|
||||
|
||||
// Check if the option exists
|
||||
if !self.options.iter().any(|opt| opt.id == chosen_option_id) {
|
||||
eprintln!(
|
||||
"Chosen option ID {} does not exist for proposal '{}'",
|
||||
chosen_option_id, self.title
|
||||
);
|
||||
return self;
|
||||
}
|
||||
|
||||
// Check eligibility for private proposals
|
||||
if let Some(group) = &self.private_group {
|
||||
if !group.contains(&user_id) {
|
||||
eprintln!(
|
||||
"User {} is not eligible to vote on proposal '{}'",
|
||||
user_id, self.title
|
||||
);
|
||||
return self;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new ballot with the comment
|
||||
let mut new_ballot = Ballot::new(ballot_id, user_id, chosen_option_id, shares);
|
||||
new_ballot.comment = Some(comment.to_string());
|
||||
|
||||
// Add the ballot to the proposal
|
||||
self.ballots.push(new_ballot);
|
||||
|
||||
// Update the vote count for the chosen option
|
||||
if let Some(option) = self
|
||||
.options
|
||||
.iter_mut()
|
||||
.find(|opt| opt.id == chosen_option_id)
|
||||
{
|
||||
option.count += shares;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ActivityType {
|
||||
ProposalCreated,
|
||||
VoteCast,
|
||||
VotingStarted,
|
||||
VotingEnded,
|
||||
}
|
||||
|
||||
impl ToString for ActivityType {
|
||||
fn to_string(&self) -> String {
|
||||
match self {
|
||||
ActivityType::ProposalCreated => "proposal_created",
|
||||
ActivityType::VoteCast => "vote_cast",
|
||||
ActivityType::VotingStarted => "voting_started",
|
||||
ActivityType::VotingEnded => "voting_ended",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a governance activity in the system
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
|
||||
#[rhai_model_export(db_type = "std::sync::Arc<crate::db::hero::OurDB>")]
|
||||
#[model] // Has base.Base in V spec
|
||||
pub struct Activity {
|
||||
/// Base model data
|
||||
pub base_data: BaseModelData,
|
||||
/// Type of activity (proposal_created, vote_cast, etc.)
|
||||
pub activity_type: String,
|
||||
/// ID of the related proposal
|
||||
pub proposal_id: u32,
|
||||
/// Title of the related proposal
|
||||
pub proposal_title: String,
|
||||
/// Name of the user who performed the action
|
||||
pub creator_name: String,
|
||||
/// Description of the activity
|
||||
pub description: String,
|
||||
/// Date and time when the activity was last updated
|
||||
pub updated_at: DateTime<Utc>,
|
||||
/// Date and time when the activity was created
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Activity {
|
||||
/// Create a default instance
|
||||
pub fn default() -> Self {
|
||||
let base_data = BaseModelData::new();
|
||||
let created_at = Utc::now();
|
||||
let updated_at = created_at;
|
||||
|
||||
Self {
|
||||
base_data,
|
||||
activity_type: String::new(),
|
||||
proposal_id: 0,
|
||||
proposal_title: String::new(),
|
||||
creator_name: String::new(),
|
||||
description: String::new(),
|
||||
updated_at,
|
||||
created_at,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new governance activity
|
||||
pub fn new(
|
||||
id: Option<u32>,
|
||||
activity_type: &str,
|
||||
proposal_id: u32,
|
||||
proposal_title: &str,
|
||||
creator_name: &str,
|
||||
description: &str,
|
||||
) -> Self {
|
||||
let mut base_data = BaseModelData::new();
|
||||
if let Some(id) = id {
|
||||
base_data.update_id(id);
|
||||
}
|
||||
|
||||
let created_at = Utc::now();
|
||||
let updated_at = created_at;
|
||||
|
||||
Self {
|
||||
base_data,
|
||||
activity_type: activity_type.to_string(),
|
||||
proposal_id,
|
||||
proposal_title: proposal_title.to_string(),
|
||||
creator_name: creator_name.to_string(),
|
||||
description: description.to_string(),
|
||||
updated_at,
|
||||
created_at,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a proposal creation activity
|
||||
pub fn proposal_created(proposal_id: u32, proposal_title: &str, creator_name: &str) -> Self {
|
||||
Self::new(
|
||||
None,
|
||||
&ActivityType::ProposalCreated.to_string(),
|
||||
proposal_id,
|
||||
proposal_title,
|
||||
creator_name,
|
||||
&format!("Proposal '{}' created by {}", proposal_title, creator_name),
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a vote cast activity
|
||||
pub fn vote_cast(proposal_id: u32, proposal_title: &str, voter_name: &str) -> Self {
|
||||
Self::new(
|
||||
None,
|
||||
&ActivityType::VoteCast.to_string(),
|
||||
proposal_id,
|
||||
proposal_title,
|
||||
voter_name,
|
||||
&format!("{} voted on proposal '{}'", voter_name, proposal_title),
|
||||
)
|
||||
}
|
||||
|
||||
// Create voting start activity
|
||||
pub fn voting_started(proposal_id: u32, proposal_title: &str) -> Self {
|
||||
Self::new(
|
||||
None,
|
||||
&ActivityType::VotingStarted.to_string(),
|
||||
proposal_id,
|
||||
proposal_title,
|
||||
"System",
|
||||
&format!("Voting started for proposal '{}'", proposal_title),
|
||||
)
|
||||
}
|
||||
|
||||
// Create voting ended activity
|
||||
pub fn voting_ended(proposal_id: u32, proposal_title: &str) -> Self {
|
||||
Self::new(
|
||||
None,
|
||||
&ActivityType::VotingEnded.to_string(),
|
||||
proposal_id,
|
||||
proposal_title,
|
||||
"System",
|
||||
&format!("Voting ended for proposal '{}'", proposal_title),
|
||||
)
|
||||
}
|
||||
}
|
296
heromodels/src/models/legal/contract.rs
Normal file
296
heromodels/src/models/legal/contract.rs
Normal file
@@ -0,0 +1,296 @@
|
||||
use heromodels_core::BaseModelData;
|
||||
use heromodels_derive::model;
|
||||
use std::fmt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// --- Enums ---
|
||||
|
||||
/// Defines the possible statuses of a contract
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ContractStatus {
|
||||
Draft,
|
||||
PendingSignatures,
|
||||
Signed,
|
||||
Active,
|
||||
Expired,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl Default for ContractStatus {
|
||||
fn default() -> Self {
|
||||
ContractStatus::Draft
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ContractStatus {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{:?}", self) // Outputs the variant name, e.g., "Draft"
|
||||
}
|
||||
}
|
||||
|
||||
/// Defines the status of a contract signer
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum SignerStatus {
|
||||
Pending,
|
||||
Signed,
|
||||
Rejected,
|
||||
}
|
||||
|
||||
impl Default for SignerStatus {
|
||||
fn default() -> Self {
|
||||
SignerStatus::Pending
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SignerStatus {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{:?}", self) // Outputs the variant name, e.g., "Pending"
|
||||
}
|
||||
}
|
||||
|
||||
// --- Structs for nested data ---
|
||||
|
||||
/// ContractRevision represents a version of the contract content
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ContractRevision {
|
||||
pub version: u32,
|
||||
pub content: String,
|
||||
pub created_at: u64, // Timestamp
|
||||
pub created_by: String,
|
||||
pub comments: Option<String>,
|
||||
}
|
||||
|
||||
impl ContractRevision {
|
||||
pub fn new(version: u32, content: String, created_at: u64, created_by: String) -> Self {
|
||||
Self {
|
||||
version,
|
||||
content,
|
||||
created_at,
|
||||
created_by,
|
||||
comments: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn comments(mut self, comments: impl ToString) -> Self {
|
||||
self.comments = Some(comments.to_string());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// ContractSigner represents a party involved in signing a contract
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ContractSigner {
|
||||
pub id: String, // Unique ID for the signer (UUID string)
|
||||
pub name: String,
|
||||
pub email: String,
|
||||
pub status: SignerStatus,
|
||||
pub signed_at: Option<u64>, // Timestamp
|
||||
pub comments: Option<String>,
|
||||
}
|
||||
|
||||
impl ContractSigner {
|
||||
pub fn new(id: String, name: String, email: String) -> Self {
|
||||
Self {
|
||||
id,
|
||||
name,
|
||||
email,
|
||||
status: SignerStatus::default(),
|
||||
signed_at: None,
|
||||
comments: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(mut self, status: SignerStatus) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn signed_at(mut self, signed_at: u64) -> Self {
|
||||
self.signed_at = Some(signed_at);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn clear_signed_at(mut self) -> Self {
|
||||
self.signed_at = None;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn comments(mut self, comments: impl ToString) -> Self {
|
||||
self.comments = Some(comments.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn clear_comments(mut self) -> Self {
|
||||
self.comments = None;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// --- Main Contract Model ---
|
||||
|
||||
/// Represents a legal agreement
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[model]
|
||||
pub struct Contract {
|
||||
pub base_data: BaseModelData, // Provides id (u32), created_at (u64), updated_at (u64)
|
||||
|
||||
#[index]
|
||||
pub contract_id: String, // Unique ID for the contract (UUID string)
|
||||
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
|
||||
#[index]
|
||||
pub contract_type: String,
|
||||
|
||||
#[index]
|
||||
pub status: crate::models::ContractStatus, // Use re-exported path for #[model] macro
|
||||
|
||||
pub created_by: String,
|
||||
pub terms_and_conditions: String,
|
||||
|
||||
pub start_date: Option<u64>,
|
||||
pub end_date: Option<u64>,
|
||||
pub renewal_period_days: Option<i32>,
|
||||
pub next_renewal_date: Option<u64>,
|
||||
|
||||
pub signers: Vec<ContractSigner>,
|
||||
pub revisions: Vec<ContractRevision>,
|
||||
pub current_version: u32,
|
||||
pub last_signed_date: Option<u64>,
|
||||
}
|
||||
|
||||
impl Contract {
|
||||
pub fn new(_base_id: u32, contract_id: String) -> Self {
|
||||
Self {
|
||||
base_data: BaseModelData::new(),
|
||||
contract_id,
|
||||
title: String::new(),
|
||||
description: String::new(),
|
||||
contract_type: String::new(),
|
||||
status: ContractStatus::default(),
|
||||
created_by: String::new(),
|
||||
terms_and_conditions: String::new(),
|
||||
start_date: None,
|
||||
end_date: None,
|
||||
renewal_period_days: None,
|
||||
next_renewal_date: None,
|
||||
signers: Vec::new(),
|
||||
revisions: Vec::new(),
|
||||
current_version: 0,
|
||||
last_signed_date: None,
|
||||
}
|
||||
}
|
||||
|
||||
// Builder methods
|
||||
pub fn title(mut self, title: impl ToString) -> Self {
|
||||
self.title = title.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn description(mut self, description: impl ToString) -> Self {
|
||||
self.description = description.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn contract_type(mut self, contract_type: impl ToString) -> Self {
|
||||
self.contract_type = contract_type.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn status(mut self, status: ContractStatus) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn created_by(mut self, created_by: impl ToString) -> Self {
|
||||
self.created_by = created_by.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn terms_and_conditions(mut self, terms: impl ToString) -> Self {
|
||||
self.terms_and_conditions = terms.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn start_date(mut self, start_date: u64) -> Self {
|
||||
self.start_date = Some(start_date);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn clear_start_date(mut self) -> Self {
|
||||
self.start_date = None;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn end_date(mut self, end_date: u64) -> Self {
|
||||
self.end_date = Some(end_date);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn clear_end_date(mut self) -> Self {
|
||||
self.end_date = None;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn renewal_period_days(mut self, days: i32) -> Self {
|
||||
self.renewal_period_days = Some(days);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn clear_renewal_period_days(mut self) -> Self {
|
||||
self.renewal_period_days = None;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn next_renewal_date(mut self, date: u64) -> Self {
|
||||
self.next_renewal_date = Some(date);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn clear_next_renewal_date(mut self) -> Self {
|
||||
self.next_renewal_date = None;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_signer(mut self, signer: ContractSigner) -> Self {
|
||||
self.signers.push(signer);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn signers(mut self, signers: Vec<ContractSigner>) -> Self {
|
||||
self.signers = signers;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_revision(mut self, revision: ContractRevision) -> Self {
|
||||
self.revisions.push(revision);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn revisions(mut self, revisions: Vec<ContractRevision>) -> Self {
|
||||
self.revisions = revisions;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn current_version(mut self, version: u32) -> Self {
|
||||
self.current_version = version;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn last_signed_date(mut self, date: u64) -> Self {
|
||||
self.last_signed_date = Some(date);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn clear_last_signed_date(mut self) -> Self {
|
||||
self.last_signed_date = None;
|
||||
self
|
||||
}
|
||||
|
||||
// Example methods for state changes
|
||||
pub fn set_status(&mut self, status: crate::models::ContractStatus) {
|
||||
self.status = status;
|
||||
// self.base_data.touch(); // Assume #[model] handles timestamp updates
|
||||
}
|
||||
}
|
5
heromodels/src/models/legal/mod.rs
Normal file
5
heromodels/src/models/legal/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod contract;
|
||||
pub mod rhai;
|
||||
|
||||
pub use contract::{Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus};
|
||||
pub use rhai::register_legal_rhai_module;
|
265
heromodels/src/models/legal/rhai.rs
Normal file
265
heromodels/src/models/legal/rhai.rs
Normal file
@@ -0,0 +1,265 @@
|
||||
use rhai::{
|
||||
Dynamic, Engine, EvalAltResult, NativeCallContext, Position, Module, Array,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::db::hero::OurDB; // Updated path based on compiler suggestion
|
||||
// use heromodels_core::BaseModelData; // Removed as fields are accessed via contract.base_data directly
|
||||
use crate::models::legal::{Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus};
|
||||
|
||||
use crate::db::Collection; // Import the Collection trait
|
||||
|
||||
// --- Helper Functions for ID and Timestamp Conversion ---
|
||||
fn i64_to_u32(val: i64, context_pos: Position, field_name: &str, object_name: &str) -> Result<u32, Box<EvalAltResult>> {
|
||||
val.try_into().map_err(|_e| {
|
||||
Box::new(EvalAltResult::ErrorArithmetic(
|
||||
format!(
|
||||
"Conversion error for field '{}' in object '{}': cannot convert i64 ({}) to u32",
|
||||
field_name,
|
||||
object_name,
|
||||
val
|
||||
),
|
||||
context_pos,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn i64_to_u64(val: i64, context_pos: Position, field_name: &str, object_name: &str) -> Result<u64, Box<EvalAltResult>> {
|
||||
val.try_into().map_err(|_e| {
|
||||
Box::new(EvalAltResult::ErrorArithmetic(
|
||||
format!(
|
||||
"Conversion error for field '{}' in object '{}': cannot convert i64 ({}) to u64",
|
||||
field_name,
|
||||
object_name,
|
||||
val
|
||||
),
|
||||
context_pos,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn i64_to_i32(val: i64, context_pos: Position, field_name: &str, object_name: &str) -> Result<i32, Box<EvalAltResult>> {
|
||||
val.try_into().map_err(|_e| {
|
||||
Box::new(EvalAltResult::ErrorArithmetic(
|
||||
format!(
|
||||
"Conversion error for field '{}' in object '{}': cannot convert i64 ({}) to i32",
|
||||
field_name,
|
||||
object_name,
|
||||
val
|
||||
),
|
||||
context_pos,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn register_legal_rhai_module(engine: &mut Engine, db: Arc<OurDB>) {
|
||||
// --- ContractStatus Enum ---
|
||||
// Register ContractStatus enum as constants
|
||||
let mut contract_status_module = Module::new();
|
||||
contract_status_module.set_var("Draft", ContractStatus::Draft);
|
||||
contract_status_module.set_var("PendingSignatures", ContractStatus::PendingSignatures);
|
||||
contract_status_module.set_var("Signed", ContractStatus::Signed);
|
||||
contract_status_module.set_var("Active", ContractStatus::Active);
|
||||
contract_status_module.set_var("Expired", ContractStatus::Expired);
|
||||
contract_status_module.set_var("Cancelled", ContractStatus::Cancelled);
|
||||
engine.register_static_module("ContractStatusConstants", contract_status_module.into());
|
||||
engine.register_type_with_name::<ContractStatus>("ContractStatus"); // Expose the type itself
|
||||
|
||||
// Register SignerStatus enum as constants
|
||||
let mut signer_status_module = Module::new();
|
||||
signer_status_module.set_var("Pending", SignerStatus::Pending);
|
||||
signer_status_module.set_var("Signed", SignerStatus::Signed);
|
||||
signer_status_module.set_var("Rejected", SignerStatus::Rejected);
|
||||
engine.register_static_module("SignerStatusConstants", signer_status_module.into());
|
||||
engine.register_type_with_name::<SignerStatus>("SignerStatus"); // Expose the type itself
|
||||
|
||||
// --- ContractRevision ---
|
||||
engine.register_type_with_name::<ContractRevision>("ContractRevision");
|
||||
engine.register_fn(
|
||||
"new_contract_revision",
|
||||
move |context: NativeCallContext, version_i64: i64, content: String, created_at_i64: i64, created_by: String| -> Result<ContractRevision, Box<EvalAltResult>> {
|
||||
let version = i64_to_u32(version_i64, context.position(), "version", "new_contract_revision")?;
|
||||
let created_at = i64_to_u64(created_at_i64, context.position(), "created_at", "new_contract_revision")?;
|
||||
Ok(ContractRevision::new(version, content, created_at, created_by))
|
||||
}
|
||||
);
|
||||
engine.register_fn("comments", |mut revision: ContractRevision, comments: String| -> ContractRevision {
|
||||
revision.comments = Some(comments);
|
||||
revision
|
||||
});
|
||||
engine.register_get("version", |revision: &mut ContractRevision| -> Result<i64, Box<EvalAltResult>> { Ok(revision.version as i64) });
|
||||
engine.register_get("content", |revision: &mut ContractRevision| -> Result<String, Box<EvalAltResult>> { Ok(revision.content.clone()) });
|
||||
engine.register_get("created_at", |revision: &mut ContractRevision| -> Result<i64, Box<EvalAltResult>> { Ok(revision.created_at as i64) });
|
||||
engine.register_get("created_by", |revision: &mut ContractRevision| -> Result<String, Box<EvalAltResult>> { Ok(revision.created_by.clone()) });
|
||||
engine.register_get("comments", |revision: &mut ContractRevision| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(revision.comments.clone().map_or(Dynamic::UNIT, Dynamic::from))
|
||||
});
|
||||
|
||||
// --- ContractSigner ---
|
||||
engine.register_type_with_name::<ContractSigner>("ContractSigner");
|
||||
engine.register_fn(
|
||||
"new_contract_signer",
|
||||
|id: String, name: String, email: String| -> ContractSigner {
|
||||
ContractSigner::new(id, name, email)
|
||||
}
|
||||
);
|
||||
engine.register_fn("status", |signer: ContractSigner, status: SignerStatus| -> ContractSigner { signer.status(status) });
|
||||
engine.register_fn("signed_at", |context: NativeCallContext, signer: ContractSigner, signed_at_i64: i64| -> Result<ContractSigner, Box<EvalAltResult>> {
|
||||
let signed_at_u64 = i64_to_u64(signed_at_i64, context.position(), "signed_at", "ContractSigner.signed_at")?;
|
||||
Ok(signer.signed_at(signed_at_u64))
|
||||
});
|
||||
engine.register_fn("clear_signed_at", |signer: ContractSigner| -> ContractSigner { signer.clear_signed_at() });
|
||||
engine.register_fn("comments", |signer: ContractSigner, comments: String| -> ContractSigner { signer.comments(comments) });
|
||||
engine.register_fn("clear_comments", |signer: ContractSigner| -> ContractSigner { signer.clear_comments() });
|
||||
|
||||
engine.register_get("id", |signer: &mut ContractSigner| -> Result<String, Box<EvalAltResult>> { Ok(signer.id.clone()) });
|
||||
engine.register_get("name", |signer: &mut ContractSigner| -> Result<String, Box<EvalAltResult>> { Ok(signer.name.clone()) });
|
||||
engine.register_get("email", |signer: &mut ContractSigner| -> Result<String, Box<EvalAltResult>> { Ok(signer.email.clone()) });
|
||||
engine.register_get("status", |signer: &mut ContractSigner| -> Result<SignerStatus, Box<EvalAltResult>> { Ok(signer.status.clone()) });
|
||||
engine.register_get("signed_at_ts", |signer: &mut ContractSigner| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(signer.signed_at.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
|
||||
});
|
||||
engine.register_get("comments", |signer: &mut ContractSigner| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(signer.comments.clone().map_or(Dynamic::UNIT, Dynamic::from))
|
||||
});
|
||||
engine.register_get("signed_at", |signer: &mut ContractSigner| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(signer.signed_at.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts)))
|
||||
});
|
||||
|
||||
// --- Contract ---
|
||||
engine.register_type_with_name::<Contract>("Contract");
|
||||
engine.register_fn(
|
||||
"new_contract",
|
||||
move |context: NativeCallContext, base_id_i64: i64, contract_id: String| -> Result<Contract, Box<EvalAltResult>> {
|
||||
let base_id = i64_to_u32(base_id_i64, context.position(), "base_id", "new_contract")?;
|
||||
Ok(Contract::new(base_id, contract_id))
|
||||
}
|
||||
);
|
||||
|
||||
// Builder methods
|
||||
engine.register_fn("title", |contract: Contract, title: String| -> Contract { contract.title(title) });
|
||||
engine.register_fn("description", |contract: Contract, description: String| -> Contract { contract.description(description) });
|
||||
engine.register_fn("contract_type", |contract: Contract, contract_type: String| -> Contract { contract.contract_type(contract_type) });
|
||||
engine.register_fn("status", |contract: Contract, status: ContractStatus| -> Contract { contract.status(status) });
|
||||
engine.register_fn("created_by", |contract: Contract, created_by: String| -> Contract { contract.created_by(created_by) });
|
||||
engine.register_fn("terms_and_conditions", |contract: Contract, terms: String| -> Contract { contract.terms_and_conditions(terms) });
|
||||
|
||||
engine.register_fn("start_date", |context: NativeCallContext, contract: Contract, start_date_i64: i64| -> Result<Contract, Box<EvalAltResult>> {
|
||||
let start_date_u64 = i64_to_u64(start_date_i64, context.position(), "start_date", "Contract.start_date")?;
|
||||
Ok(contract.start_date(start_date_u64))
|
||||
});
|
||||
engine.register_fn("clear_start_date", |contract: Contract| -> Contract { contract.clear_start_date() });
|
||||
|
||||
engine.register_fn("end_date", |context: NativeCallContext, contract: Contract, end_date_i64: i64| -> Result<Contract, Box<EvalAltResult>> {
|
||||
let end_date_u64 = i64_to_u64(end_date_i64, context.position(), "end_date", "Contract.end_date")?;
|
||||
Ok(contract.end_date(end_date_u64))
|
||||
});
|
||||
engine.register_fn("clear_end_date", |contract: Contract| -> Contract { contract.clear_end_date() });
|
||||
|
||||
engine.register_fn("renewal_period_days", |context: NativeCallContext, contract: Contract, days_i64: i64| -> Result<Contract, Box<EvalAltResult>> {
|
||||
let days_i32 = i64_to_i32(days_i64, context.position(), "renewal_period_days", "Contract.renewal_period_days")?;
|
||||
Ok(contract.renewal_period_days(days_i32))
|
||||
});
|
||||
engine.register_fn("clear_renewal_period_days", |contract: Contract| -> Contract { contract.clear_renewal_period_days() });
|
||||
|
||||
engine.register_fn("next_renewal_date", |context: NativeCallContext, contract: Contract, date_i64: i64| -> Result<Contract, Box<EvalAltResult>> {
|
||||
let date_u64 = i64_to_u64(date_i64, context.position(), "next_renewal_date", "Contract.next_renewal_date")?;
|
||||
Ok(contract.next_renewal_date(date_u64))
|
||||
});
|
||||
engine.register_fn("clear_next_renewal_date", |contract: Contract| -> Contract { contract.clear_next_renewal_date() });
|
||||
|
||||
engine.register_fn("add_signer", |contract: Contract, signer: ContractSigner| -> Contract { contract.add_signer(signer) });
|
||||
engine.register_fn("signers", |contract: Contract, signers_array: Array| -> Contract {
|
||||
let signers_vec = signers_array.into_iter().filter_map(|s| s.try_cast::<ContractSigner>()).collect();
|
||||
contract.signers(signers_vec)
|
||||
});
|
||||
|
||||
engine.register_fn("add_revision", |contract: Contract, revision: ContractRevision| -> Contract { contract.add_revision(revision) });
|
||||
engine.register_fn("revisions", |contract: Contract, revisions_array: Array| -> Contract {
|
||||
let revisions_vec = revisions_array.into_iter().filter_map(|r| r.try_cast::<ContractRevision>()).collect();
|
||||
contract.revisions(revisions_vec)
|
||||
});
|
||||
|
||||
engine.register_fn("current_version", |context: NativeCallContext, contract: Contract, version_i64: i64| -> Result<Contract, Box<EvalAltResult>> {
|
||||
let version_u32 = i64_to_u32(version_i64, context.position(), "current_version", "Contract.current_version")?;
|
||||
Ok(contract.current_version(version_u32))
|
||||
});
|
||||
|
||||
engine.register_fn("last_signed_date", |context: NativeCallContext, contract: Contract, date_i64: i64| -> Result<Contract, Box<EvalAltResult>> {
|
||||
let date_u64 = i64_to_u64(date_i64, context.position(), "last_signed_date", "Contract.last_signed_date")?;
|
||||
Ok(contract.last_signed_date(date_u64))
|
||||
});
|
||||
engine.register_fn("clear_last_signed_date", |contract: Contract| -> Contract { contract.clear_last_signed_date() });
|
||||
|
||||
// Getters for Contract
|
||||
engine.register_get("id", |contract: &mut Contract| -> Result<i64, Box<EvalAltResult>> { Ok(contract.base_data.id as i64) });
|
||||
engine.register_get("created_at_ts", |contract: &mut Contract| -> Result<i64, Box<EvalAltResult>> { Ok(contract.base_data.created_at as i64) });
|
||||
engine.register_get("updated_at_ts", |contract: &mut Contract| -> Result<i64, Box<EvalAltResult>> { Ok(contract.base_data.modified_at as i64) });
|
||||
engine.register_get("contract_id", |contract: &mut Contract| -> Result<String, Box<EvalAltResult>> { Ok(contract.contract_id.clone()) });
|
||||
engine.register_get("title", |contract: &mut Contract| -> Result<String, Box<EvalAltResult>> { Ok(contract.title.clone()) });
|
||||
engine.register_get("description", |contract: &mut Contract| -> Result<String, Box<EvalAltResult>> { Ok(contract.description.clone()) });
|
||||
engine.register_get("contract_type", |contract: &mut Contract| -> Result<String, Box<EvalAltResult>> { Ok(contract.contract_type.clone()) });
|
||||
engine.register_get("status", |contract: &mut Contract| -> Result<ContractStatus, Box<EvalAltResult>> { Ok(contract.status.clone()) });
|
||||
engine.register_get("created_by", |contract: &mut Contract| -> Result<String, Box<EvalAltResult>> { Ok(contract.created_by.clone()) });
|
||||
engine.register_get("terms_and_conditions", |contract: &mut Contract| -> Result<String, Box<EvalAltResult>> { Ok(contract.terms_and_conditions.clone()) });
|
||||
|
||||
engine.register_get("start_date", |contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(contract.start_date.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
|
||||
});
|
||||
engine.register_get("end_date", |contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(contract.end_date.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
|
||||
});
|
||||
engine.register_get("renewal_period_days", |contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(contract.renewal_period_days.map_or(Dynamic::UNIT, |days| Dynamic::from(days as i64)))
|
||||
});
|
||||
engine.register_get("next_renewal_date", |contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(contract.next_renewal_date.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
|
||||
});
|
||||
engine.register_get("last_signed_date", |contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
Ok(contract.last_signed_date.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
|
||||
});
|
||||
|
||||
engine.register_get("current_version", |contract: &mut Contract| -> Result<i64, Box<EvalAltResult>> { Ok(contract.current_version as i64) });
|
||||
|
||||
engine.register_get("signers", |contract: &mut Contract| -> Result<Array, Box<EvalAltResult>> {
|
||||
let rhai_array = contract.signers.iter().cloned().map(Dynamic::from).collect::<Array>();
|
||||
Ok(rhai_array)
|
||||
});
|
||||
engine.register_get("revisions", |contract: &mut Contract| -> Result<Array, Box<EvalAltResult>> {
|
||||
let rhai_array = contract.revisions.iter().cloned().map(Dynamic::from).collect::<Array>();
|
||||
Ok(rhai_array)
|
||||
});
|
||||
|
||||
// Method set_status
|
||||
engine.register_fn("set_contract_status", |contract: &mut Contract, status: ContractStatus| {
|
||||
contract.set_status(status);
|
||||
});
|
||||
|
||||
// --- Database Interaction ---
|
||||
let captured_db_for_set = Arc::clone(&db);
|
||||
engine.register_fn("set_contract",
|
||||
move |contract: Contract| -> Result<(), Box<EvalAltResult>> {
|
||||
captured_db_for_set.set(&contract).map(|_| ()).map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to set Contract (ID: {}): {}", contract.base_data.id, e).into(),
|
||||
Position::NONE,
|
||||
))
|
||||
})
|
||||
});
|
||||
|
||||
let captured_db_for_get = Arc::clone(&db);
|
||||
engine.register_fn("get_contract_by_id",
|
||||
move |context: NativeCallContext, id_i64: i64| -> Result<Contract, Box<EvalAltResult>> {
|
||||
let id_u32 = i64_to_u32(id_i64, context.position(), "id", "get_contract_by_id")?;
|
||||
|
||||
captured_db_for_get.get_by_id(id_u32)
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Error getting Contract (ID: {}): {}", id_u32, e).into(),
|
||||
Position::NONE,
|
||||
)))?
|
||||
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Contract with ID {} not found", id_u32).into(),
|
||||
Position::NONE,
|
||||
)))
|
||||
});
|
||||
}
|
@@ -1,8 +1,33 @@
|
||||
// Export submodules
|
||||
pub mod core;
|
||||
pub mod userexample;
|
||||
// pub mod productexample; // Temporarily remove as files are missing
|
||||
pub mod biz;
|
||||
pub mod calendar;
|
||||
pub mod finance;
|
||||
pub mod flow;
|
||||
pub mod governance;
|
||||
pub mod legal;
|
||||
pub mod projects;
|
||||
|
||||
// Re-export key types for convenience
|
||||
pub use core::Comment;
|
||||
pub use userexample::User;
|
||||
// pub use productexample::Product; // Temporarily remove
|
||||
pub use biz::{Sale, SaleItem, SaleStatus};
|
||||
pub use calendar::{AttendanceStatus, Attendee, Calendar, Event};
|
||||
pub use finance::marketplace::{Bid, BidStatus, Listing, ListingStatus, ListingType};
|
||||
pub use finance::{Account, Asset, AssetType};
|
||||
pub use flow::{Flow, FlowStep, SignatureRequirement};
|
||||
pub use governance::{
|
||||
Activity, ActivityType, Ballot, Proposal, ProposalStatus, VoteEventStatus, VoteOption,
|
||||
};
|
||||
pub use legal::{Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus};
|
||||
|
||||
#[cfg(feature = "rhai")]
|
||||
pub use biz::register_biz_rhai_module;
|
||||
pub use calendar::register_calendar_rhai_module;
|
||||
pub use flow::register_flow_rhai_module;
|
||||
pub use legal::register_legal_rhai_module;
|
||||
#[cfg(feature = "rhai")]
|
||||
pub use projects::register_projects_rhai_module;
|
||||
|
229
heromodels/src/models/projects/base.rs
Normal file
229
heromodels/src/models/projects/base.rs
Normal file
@@ -0,0 +1,229 @@
|
||||
// heromodels/src/models/projects/base.rs
|
||||
use serde::{Deserialize, Serialize};
|
||||
use heromodels_core::{BaseModelData, Model, BaseModelDataOps};
|
||||
#[cfg(feature = "rhai")]
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use strum_macros::Display; // Made unconditional as Display derive is used on non-rhai-gated enums
|
||||
|
||||
// --- Enums ---
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Display)]
|
||||
pub enum Priority {
|
||||
Critical,
|
||||
High,
|
||||
Medium,
|
||||
Low,
|
||||
None,
|
||||
}
|
||||
|
||||
impl Default for Priority {
|
||||
fn default() -> Self {
|
||||
Priority::None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Display)]
|
||||
pub enum Status {
|
||||
Todo,
|
||||
InProgress,
|
||||
Review,
|
||||
Done,
|
||||
Archived,
|
||||
}
|
||||
|
||||
impl Default for Status {
|
||||
fn default() -> Self {
|
||||
Status::Todo
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Display)]
|
||||
pub enum ItemType {
|
||||
Epic,
|
||||
Story,
|
||||
Task,
|
||||
Bug,
|
||||
Improvement,
|
||||
Feature,
|
||||
}
|
||||
|
||||
impl Default for ItemType {
|
||||
fn default() -> Self {
|
||||
ItemType::Task
|
||||
}
|
||||
}
|
||||
|
||||
// --- Structs ---
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "rhai", derive(CustomType))]
|
||||
pub struct Project {
|
||||
pub base_data: BaseModelData,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub owner_id: u32,
|
||||
pub member_ids: Vec<u32>,
|
||||
pub board_ids: Vec<u32>,
|
||||
pub sprint_ids: Vec<u32>,
|
||||
pub epic_ids: Vec<u32>,
|
||||
pub tags: Vec<String>,
|
||||
pub status: Status,
|
||||
pub priority: Priority,
|
||||
pub item_type: ItemType,
|
||||
}
|
||||
|
||||
impl BaseModelDataOps for Project {
|
||||
fn get_base_data_mut(&mut self) -> &mut BaseModelData {
|
||||
&mut self.base_data
|
||||
}
|
||||
}
|
||||
|
||||
impl Model for Project {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.base_data.id
|
||||
}
|
||||
fn db_prefix() -> &'static str {
|
||||
"prj"
|
||||
}
|
||||
fn base_data_mut(&mut self) -> &mut BaseModelData {
|
||||
&mut self.base_data
|
||||
}
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn new(_id: u32, name: String, description: String, owner_id: u32) -> Self {
|
||||
Self {
|
||||
base_data: BaseModelData::new(),
|
||||
name,
|
||||
description,
|
||||
owner_id,
|
||||
member_ids: Vec::new(),
|
||||
board_ids: Vec::new(),
|
||||
sprint_ids: Vec::new(),
|
||||
epic_ids: Vec::new(),
|
||||
tags: Vec::new(),
|
||||
status: Status::default(),
|
||||
priority: Priority::default(),
|
||||
item_type: ItemType::default(),
|
||||
}
|
||||
}
|
||||
|
||||
// Builder methods
|
||||
pub fn name(mut self, name: String) -> Self {
|
||||
self.name = name;
|
||||
self
|
||||
}
|
||||
pub fn description(mut self, description: String) -> Self {
|
||||
self.description = description;
|
||||
self
|
||||
}
|
||||
pub fn owner_id(mut self, owner_id: u32) -> Self {
|
||||
self.owner_id = owner_id;
|
||||
self
|
||||
}
|
||||
pub fn add_member_id(mut self, member_id: u32) -> Self {
|
||||
self.member_ids.push(member_id);
|
||||
self
|
||||
}
|
||||
pub fn member_ids(mut self, member_ids: Vec<u32>) -> Self {
|
||||
self.member_ids = member_ids;
|
||||
self
|
||||
}
|
||||
pub fn add_board_id(mut self, board_id: u32) -> Self {
|
||||
self.board_ids.push(board_id);
|
||||
self
|
||||
}
|
||||
pub fn board_ids(mut self, board_ids: Vec<u32>) -> Self {
|
||||
self.board_ids = board_ids;
|
||||
self
|
||||
}
|
||||
pub fn add_sprint_id(mut self, sprint_id: u32) -> Self {
|
||||
self.sprint_ids.push(sprint_id);
|
||||
self
|
||||
}
|
||||
pub fn sprint_ids(mut self, sprint_ids: Vec<u32>) -> Self {
|
||||
self.sprint_ids = sprint_ids;
|
||||
self
|
||||
}
|
||||
pub fn add_epic_id(mut self, epic_id: u32) -> Self {
|
||||
self.epic_ids.push(epic_id);
|
||||
self
|
||||
}
|
||||
pub fn epic_ids(mut self, epic_ids: Vec<u32>) -> Self {
|
||||
self.epic_ids = epic_ids;
|
||||
self
|
||||
}
|
||||
pub fn add_tag(mut self, tag: String) -> Self {
|
||||
self.tags.push(tag);
|
||||
self
|
||||
}
|
||||
pub fn tags(mut self, tags: Vec<String>) -> Self {
|
||||
self.tags = tags;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn status(mut self, status: Status) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn priority(mut self, priority: Priority) -> Self {
|
||||
self.priority = priority;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn item_type(mut self, item_type: ItemType) -> Self {
|
||||
self.item_type = item_type;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[cfg_attr(feature = "rhai", derive(CustomType))]
|
||||
pub struct Label {
|
||||
pub base_data: BaseModelData,
|
||||
pub name: String,
|
||||
pub color: String, // Hex color code
|
||||
}
|
||||
|
||||
impl BaseModelDataOps for Label {
|
||||
fn get_base_data_mut(&mut self) -> &mut BaseModelData {
|
||||
&mut self.base_data
|
||||
}
|
||||
}
|
||||
|
||||
impl Model for Label {
|
||||
fn get_id(&self) -> u32 {
|
||||
self.base_data.id
|
||||
}
|
||||
fn db_prefix() -> &'static str {
|
||||
"lbl"
|
||||
}
|
||||
fn base_data_mut(&mut self) -> &mut BaseModelData {
|
||||
&mut self.base_data
|
||||
}
|
||||
}
|
||||
|
||||
impl Label {
|
||||
pub fn new(id: u32, name: String, color: String) -> Self {
|
||||
let mut base_data = BaseModelData::new();
|
||||
base_data.update_id(id);
|
||||
Self {
|
||||
base_data,
|
||||
name,
|
||||
color,
|
||||
}
|
||||
}
|
||||
|
||||
// Builder methods
|
||||
pub fn name(mut self, name: String) -> Self {
|
||||
self.name = name;
|
||||
self
|
||||
}
|
||||
pub fn color(mut self, color: String) -> Self {
|
||||
self.color = color;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
21
heromodels/src/models/projects/mod.rs
Normal file
21
heromodels/src/models/projects/mod.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
// heromodels/src/models/projects/mod.rs
|
||||
|
||||
pub mod base;
|
||||
// pub mod epic;
|
||||
// pub mod issue;
|
||||
// pub mod kanban;
|
||||
// pub mod sprint;
|
||||
// pub mod story;
|
||||
|
||||
pub use base::*;
|
||||
// pub use epic::*;
|
||||
// pub use issue::*;
|
||||
// pub use kanban::*;
|
||||
// pub use sprint::*;
|
||||
// pub use story::*;
|
||||
|
||||
#[cfg(feature = "rhai")]
|
||||
pub mod rhai;
|
||||
|
||||
#[cfg(feature = "rhai")]
|
||||
pub use rhai::register_projects_rhai_module;
|
256
heromodels/src/models/projects/rhai.rs
Normal file
256
heromodels/src/models/projects/rhai.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
// heromodels/src/models/projects/rhai.rs
|
||||
|
||||
use rhai::{Engine, EvalAltResult, Dynamic, Position};
|
||||
use std::sync::Arc;
|
||||
use crate::db::hero::OurDB;
|
||||
use heromodels_core::{Model, BaseModelDataOps};
|
||||
use crate::db::{Db, Collection};
|
||||
|
||||
|
||||
// Import models from the projects::base module
|
||||
use super::base::{Project, /* Label, */ Priority, Status, ItemType}; // Label commented out as it's unused for now
|
||||
|
||||
// Helper function for ID conversion (if needed, similar to other rhai.rs files)
|
||||
fn id_from_i64(val: i64) -> Result<u32, Box<EvalAltResult>> {
|
||||
if val < 0 {
|
||||
Err(EvalAltResult::ErrorArithmetic(
|
||||
format!("ID value cannot be negative: {}", val),
|
||||
rhai::Position::NONE,
|
||||
)
|
||||
.into())
|
||||
} else {
|
||||
Ok(val as u32)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_projects_rhai_module(engine: &mut Engine, db: Arc<OurDB>) {
|
||||
// Register enums as constants (example for Priority)
|
||||
engine.register_static_module("Priority", {
|
||||
let mut module = rhai::Module::new();
|
||||
module.set_var("Critical", Priority::Critical);
|
||||
module.set_var("High", Priority::High);
|
||||
module.set_var("Medium", Priority::Medium);
|
||||
module.set_var("Low", Priority::Low);
|
||||
module.set_var("None", Priority::None);
|
||||
module.into()
|
||||
});
|
||||
|
||||
engine.register_static_module("Status", {
|
||||
let mut module = rhai::Module::new();
|
||||
module.set_var("Todo", Status::Todo);
|
||||
module.set_var("InProgress", Status::InProgress);
|
||||
module.set_var("Review", Status::Review);
|
||||
module.set_var("Done", Status::Done);
|
||||
module.set_var("Archived", Status::Archived);
|
||||
module.into()
|
||||
});
|
||||
|
||||
engine.register_static_module("ItemType", {
|
||||
let mut module = rhai::Module::new();
|
||||
module.set_var("Epic", ItemType::Epic);
|
||||
module.set_var("Story", ItemType::Story);
|
||||
module.set_var("Task", ItemType::Task);
|
||||
module.set_var("Bug", ItemType::Bug);
|
||||
module.set_var("Improvement", ItemType::Improvement);
|
||||
module.set_var("Feature", ItemType::Feature);
|
||||
module.into()
|
||||
});
|
||||
|
||||
// --- Enum Type Registration ---
|
||||
engine.register_type_with_name::<Priority>("Priority");
|
||||
engine.register_fn("to_string", |p: &mut Priority| ToString::to_string(p));
|
||||
|
||||
engine.register_type_with_name::<Status>("Status");
|
||||
engine.register_fn("to_string", |s: &mut Status| ToString::to_string(s));
|
||||
|
||||
engine.register_type_with_name::<ItemType>("ItemType");
|
||||
engine.register_fn("to_string", |it: &mut ItemType| ToString::to_string(it));
|
||||
|
||||
// --- Project Registration ---
|
||||
engine.register_type_with_name::<Project>("Project");
|
||||
|
||||
// Constructor for Project
|
||||
// Zero-argument constructor
|
||||
engine.register_fn("new_project", || -> Result<Project, Box<EvalAltResult>> {
|
||||
// Assuming Project::new() or Project::default() can be used.
|
||||
// If Project::new() requires args, this needs adjustment or Project needs Default impl.
|
||||
Ok(Project::new(0, "".to_string(), "".to_string(), 0))
|
||||
});
|
||||
|
||||
// Multi-argument constructor (renamed)
|
||||
engine.register_fn("new_project_with_details", |id_i64: i64, name: String, description: String, owner_id_i64: i64| -> Result<Project, Box<EvalAltResult>> {
|
||||
Ok(Project::new(id_from_i64(id_i64)?, name, description, id_from_i64(owner_id_i64)?))
|
||||
});
|
||||
|
||||
// Getters for Project
|
||||
engine.register_get("id", |p: &mut Project| -> Result<i64, Box<EvalAltResult>> { Ok(p.get_id() as i64) });
|
||||
engine.register_get("name", |p: &mut Project| -> Result<String, Box<EvalAltResult>> { Ok(p.name.clone()) });
|
||||
engine.register_get("description", |p: &mut Project| -> Result<String, Box<EvalAltResult>> { Ok(p.description.clone()) });
|
||||
engine.register_get("owner_id", |p: &mut Project| -> Result<i64, Box<EvalAltResult>> { Ok(p.owner_id as i64) });
|
||||
engine.register_get("member_ids", |p: &mut Project| -> Result<rhai::Array, Box<EvalAltResult>> {
|
||||
Ok(p.member_ids.iter().map(|&id| rhai::Dynamic::from(id as i64)).collect())
|
||||
});
|
||||
engine.register_get("board_ids", |p: &mut Project| -> Result<rhai::Array, Box<EvalAltResult>> {
|
||||
Ok(p.board_ids.iter().map(|&id| rhai::Dynamic::from(id as i64)).collect())
|
||||
});
|
||||
engine.register_get("sprint_ids", |p: &mut Project| -> Result<rhai::Array, Box<EvalAltResult>> {
|
||||
Ok(p.sprint_ids.iter().map(|&id| rhai::Dynamic::from(id as i64)).collect())
|
||||
});
|
||||
engine.register_get("epic_ids", |p: &mut Project| -> Result<rhai::Array, Box<EvalAltResult>> {
|
||||
Ok(p.epic_ids.iter().map(|&id| rhai::Dynamic::from(id as i64)).collect())
|
||||
});
|
||||
engine.register_get("tags", |p: &mut Project| -> Result<rhai::Array, Box<EvalAltResult>> {
|
||||
Ok(p.tags.iter().map(|tag| rhai::Dynamic::from(tag.clone())).collect())
|
||||
});
|
||||
engine.register_get("created_at", |p: &mut Project| -> Result<i64, Box<EvalAltResult>> { Ok(p.base_data.created_at) });
|
||||
engine.register_get("modified_at", |p: &mut Project| -> Result<i64, Box<EvalAltResult>> { Ok(p.base_data.modified_at) });
|
||||
engine.register_get("comments", |p: &mut Project| -> Result<rhai::Array, Box<EvalAltResult>> {
|
||||
Ok(p.base_data.comments.iter().map(|&id| rhai::Dynamic::from(id as i64)).collect())
|
||||
});
|
||||
engine.register_get("status", |p: &mut Project| -> Result<Status, Box<EvalAltResult>> { Ok(p.status.clone()) });
|
||||
engine.register_get("priority", |p: &mut Project| -> Result<Priority, Box<EvalAltResult>> { Ok(p.priority.clone()) });
|
||||
engine.register_get("item_type", |p: &mut Project| -> Result<ItemType, Box<EvalAltResult>> { Ok(p.item_type.clone()) });
|
||||
|
||||
// Builder methods for Project
|
||||
// let db_clone = db.clone(); // This was unused
|
||||
engine.register_fn("name", |p: Project, name: String| -> Result<Project, Box<EvalAltResult>> { Ok(p.name(name)) });
|
||||
engine.register_fn("description", |p: Project, description: String| -> Result<Project, Box<EvalAltResult>> { Ok(p.description(description)) });
|
||||
engine.register_fn("owner_id", |p: Project, owner_id_i64: i64| -> Result<Project, Box<EvalAltResult>> { Ok(p.owner_id(id_from_i64(owner_id_i64)?)) });
|
||||
engine.register_fn("add_member_id", |p: Project, member_id_i64: i64| -> Result<Project, Box<EvalAltResult>> { Ok(p.add_member_id(id_from_i64(member_id_i64)?)) });
|
||||
engine.register_fn("member_ids", |p: Project, member_ids_i64: rhai::Array| -> Result<Project, Box<EvalAltResult>> {
|
||||
let ids = member_ids_i64
|
||||
.into_iter()
|
||||
.map(|id_dyn: Dynamic| {
|
||||
let val_i64 = id_dyn.clone().try_cast::<i64>().ok_or_else(|| {
|
||||
Box::new(EvalAltResult::ErrorMismatchDataType(
|
||||
"Expected integer for ID".to_string(),
|
||||
id_dyn.type_name().to_string(),
|
||||
Position::NONE,
|
||||
))
|
||||
})?;
|
||||
id_from_i64(val_i64)
|
||||
})
|
||||
.collect::<Result<Vec<u32>, Box<EvalAltResult>>>()?;
|
||||
Ok(p.member_ids(ids))
|
||||
});
|
||||
engine.register_fn("add_board_id", |p: Project, board_id_i64: i64| -> Result<Project, Box<EvalAltResult>> { Ok(p.add_board_id(id_from_i64(board_id_i64)?)) });
|
||||
engine.register_fn("board_ids", |p: Project, board_ids_i64: rhai::Array| -> Result<Project, Box<EvalAltResult>> {
|
||||
let ids = board_ids_i64
|
||||
.into_iter()
|
||||
.map(|id_dyn: Dynamic| {
|
||||
let val_i64 = id_dyn.clone().try_cast::<i64>().ok_or_else(|| {
|
||||
Box::new(EvalAltResult::ErrorMismatchDataType(
|
||||
"Expected integer for ID".to_string(),
|
||||
id_dyn.type_name().to_string(),
|
||||
Position::NONE,
|
||||
))
|
||||
})?;
|
||||
id_from_i64(val_i64)
|
||||
})
|
||||
.collect::<Result<Vec<u32>, Box<EvalAltResult>>>()?;
|
||||
Ok(p.board_ids(ids))
|
||||
});
|
||||
engine.register_fn("add_sprint_id", |p: Project, sprint_id_i64: i64| -> Result<Project, Box<EvalAltResult>> { Ok(p.add_sprint_id(id_from_i64(sprint_id_i64)?)) });
|
||||
engine.register_fn("sprint_ids", |p: Project, sprint_ids_i64: rhai::Array| -> Result<Project, Box<EvalAltResult>> {
|
||||
let ids = sprint_ids_i64
|
||||
.into_iter()
|
||||
.map(|id_dyn: Dynamic| {
|
||||
let val_i64 = id_dyn.clone().try_cast::<i64>().ok_or_else(|| {
|
||||
Box::new(EvalAltResult::ErrorMismatchDataType(
|
||||
"Expected integer for ID".to_string(),
|
||||
id_dyn.type_name().to_string(),
|
||||
Position::NONE,
|
||||
))
|
||||
})?;
|
||||
id_from_i64(val_i64)
|
||||
})
|
||||
.collect::<Result<Vec<u32>, Box<EvalAltResult>>>()?;
|
||||
Ok(p.sprint_ids(ids))
|
||||
});
|
||||
engine.register_fn("add_epic_id", |p: Project, epic_id_i64: i64| -> Result<Project, Box<EvalAltResult>> { Ok(p.add_epic_id(id_from_i64(epic_id_i64)?)) });
|
||||
engine.register_fn("epic_ids", |p: Project, epic_ids_i64: rhai::Array| -> Result<Project, Box<EvalAltResult>> {
|
||||
let ids = epic_ids_i64
|
||||
.into_iter()
|
||||
.map(|id_dyn: Dynamic| {
|
||||
let val_i64 = id_dyn.clone().try_cast::<i64>().ok_or_else(|| {
|
||||
Box::new(EvalAltResult::ErrorMismatchDataType(
|
||||
"Expected integer for ID".to_string(),
|
||||
id_dyn.type_name().to_string(),
|
||||
Position::NONE,
|
||||
))
|
||||
})?;
|
||||
id_from_i64(val_i64)
|
||||
})
|
||||
.collect::<Result<Vec<u32>, Box<EvalAltResult>>>()?;
|
||||
Ok(p.epic_ids(ids))
|
||||
});
|
||||
engine.register_fn("add_tag", |p: Project, tag: String| -> Result<Project, Box<EvalAltResult>> { Ok(p.add_tag(tag)) });
|
||||
engine.register_fn("tags", |p: Project, tags_dyn: rhai::Array| -> Result<Project, Box<EvalAltResult>> {
|
||||
let tags_vec = tags_dyn
|
||||
.into_iter()
|
||||
.map(|tag_dyn: Dynamic| {
|
||||
tag_dyn.clone().into_string().map_err(|_err| { // _err is Rhai's internal error, we create a new one
|
||||
Box::new(EvalAltResult::ErrorMismatchDataType(
|
||||
"Expected string for tag".to_string(),
|
||||
tag_dyn.type_name().to_string(),
|
||||
Position::NONE,
|
||||
))
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<String>, Box<EvalAltResult>>>()?;
|
||||
Ok(p.tags(tags_vec))
|
||||
});
|
||||
|
||||
engine.register_fn("status", |p: Project, status: Status| -> Result<Project, Box<EvalAltResult>> { Ok(p.status(status)) });
|
||||
engine.register_fn("priority", |p: Project, priority: Priority| -> Result<Project, Box<EvalAltResult>> { Ok(p.priority(priority)) });
|
||||
engine.register_fn("item_type", |p: Project, item_type: ItemType| -> Result<Project, Box<EvalAltResult>> { Ok(p.item_type(item_type)) });
|
||||
// Base ModelData builders
|
||||
engine.register_fn("add_base_comment", |p: Project, comment_id_i64: i64| -> Result<Project, Box<EvalAltResult>> { Ok(p.add_base_comment(id_from_i64(comment_id_i64)?)) });
|
||||
|
||||
// --- Database Interaction Functions ---
|
||||
let db_clone_set = db.clone();
|
||||
engine.register_fn("set_project", move |project: Project| -> Result<(), Box<EvalAltResult>> {
|
||||
let collection = db_clone_set.collection::<Project>().map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorSystem(
|
||||
"Failed to access project collection".to_string(),
|
||||
format!("DB operation failed: {:?}", e).into(),
|
||||
))
|
||||
})?;
|
||||
|
||||
collection.set(&project).map(|_| ()).map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorSystem(
|
||||
"Failed to save project".to_string(),
|
||||
format!("DB operation failed: {:?}", e).into(),
|
||||
))
|
||||
})
|
||||
});
|
||||
|
||||
let db_clone_get = db.clone();
|
||||
engine.register_fn("get_project_by_id", move |id_i64: i64| -> Result<Dynamic, Box<EvalAltResult>> {
|
||||
let id = id_from_i64(id_i64)?;
|
||||
let collection = db_clone_get.collection::<Project>().map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorSystem(
|
||||
"Failed to access project collection".to_string(),
|
||||
format!("DB operation failed: {:?}", e).into(),
|
||||
))
|
||||
})?;
|
||||
|
||||
match collection.get_by_id(id) {
|
||||
Ok(Some(project)) => Ok(Dynamic::from(project)),
|
||||
Ok(None) => Ok(Dynamic::UNIT), // Represents '()' in Rhai
|
||||
Err(e) => Err(Box::new(EvalAltResult::ErrorSystem(
|
||||
"Failed to retrieve project by ID".to_string(),
|
||||
format!("DB operation failed: {:?}", e).into(),
|
||||
))),
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: Register Rhai bindings for the `Label` model if needed, or remove unused import.
|
||||
// Register Label type and its methods/getters
|
||||
// engine.register_type_with_name::<Label>("Label")
|
||||
// .register_fn("new_label", Label::new) // Simplified
|
||||
// // ... other Label methods and getters ...
|
||||
// ;
|
||||
|
||||
// TODO: Add DB interaction functions like set_project, get_project_by_id etc.
|
||||
}
|
@@ -26,10 +26,10 @@ pub struct User {
|
||||
}
|
||||
|
||||
impl User {
|
||||
/// Create a new user
|
||||
pub fn new(id: u32) -> Self {
|
||||
/// Create a new user with auto-generated ID
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
base_data: BaseModelData::new(id),
|
||||
base_data: BaseModelData::new(),
|
||||
username: String::new(),
|
||||
email: String::new(),
|
||||
full_name: String::new(),
|
||||
|
1
heromodels/temp_calendar_db/data/lookup/.inc
Normal file
1
heromodels/temp_calendar_db/data/lookup/.inc
Normal file
@@ -0,0 +1 @@
|
||||
1
|
BIN
heromodels/temp_calendar_db/data/lookup/data
Normal file
BIN
heromodels/temp_calendar_db/data/lookup/data
Normal file
Binary file not shown.
BIN
heromodels/temp_calendar_db/index/0.db
Normal file
BIN
heromodels/temp_calendar_db/index/0.db
Normal file
Binary file not shown.
1
heromodels/temp_calendar_db/index/lookup/.inc
Normal file
1
heromodels/temp_calendar_db/index/lookup/.inc
Normal file
@@ -0,0 +1 @@
|
||||
2
|
BIN
heromodels/temp_calendar_db/index/lookup/data
Normal file
BIN
heromodels/temp_calendar_db/index/lookup/data
Normal file
Binary file not shown.
1
heromodels/temp_governance_db/data/lookup/.inc
Normal file
1
heromodels/temp_governance_db/data/lookup/.inc
Normal file
@@ -0,0 +1 @@
|
||||
1
|
BIN
heromodels/temp_governance_db/data/lookup/data
Normal file
BIN
heromodels/temp_governance_db/data/lookup/data
Normal file
Binary file not shown.
BIN
heromodels/temp_governance_db/index/0.db
Normal file
BIN
heromodels/temp_governance_db/index/0.db
Normal file
Binary file not shown.
1
heromodels/temp_governance_db/index/lookup/.inc
Normal file
1
heromodels/temp_governance_db/index/lookup/.inc
Normal file
@@ -0,0 +1 @@
|
||||
2
|
BIN
heromodels/temp_governance_db/index/lookup/data
Normal file
BIN
heromodels/temp_governance_db/index/lookup/data
Normal file
Binary file not shown.
BIN
heromodels/test_rhai_db/data/lookup/data
Normal file
BIN
heromodels/test_rhai_db/data/lookup/data
Normal file
Binary file not shown.
BIN
heromodels/test_rhai_db/index/0.db
Normal file
BIN
heromodels/test_rhai_db/index/0.db
Normal file
Binary file not shown.
1
heromodels/test_rhai_db/index/lookup/.inc
Normal file
1
heromodels/test_rhai_db/index/lookup/.inc
Normal file
@@ -0,0 +1 @@
|
||||
2
|
BIN
heromodels/test_rhai_db/index/lookup/data
Normal file
BIN
heromodels/test_rhai_db/index/lookup/data
Normal file
Binary file not shown.
30
heromodels_core/src/base_data_builder.rs
Normal file
30
heromodels_core/src/base_data_builder.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use crate::BaseModelData;
|
||||
|
||||
pub trait BaseModelDataOps: Sized {
|
||||
fn get_base_data_mut(&mut self) -> &mut BaseModelData;
|
||||
|
||||
fn set_base_id(mut self, id: u32) -> Self {
|
||||
self.get_base_data_mut().id = id;
|
||||
self
|
||||
}
|
||||
|
||||
fn set_base_created_at(mut self, time: i64) -> Self {
|
||||
self.get_base_data_mut().created_at = time;
|
||||
self
|
||||
}
|
||||
|
||||
fn set_base_modified_at(mut self, time: i64) -> Self {
|
||||
self.get_base_data_mut().modified_at = time;
|
||||
self
|
||||
}
|
||||
|
||||
fn add_base_comment(mut self, comment_id: u32) -> Self {
|
||||
self.get_base_data_mut().comments.push(comment_id);
|
||||
self
|
||||
}
|
||||
|
||||
fn set_base_comments(mut self, comment_ids: Vec<u32>) -> Self {
|
||||
self.get_base_data_mut().comments = comment_ids;
|
||||
self
|
||||
}
|
||||
}
|
@@ -64,15 +64,6 @@ pub trait Model:
|
||||
/// Get a mutable reference to the base_data field
|
||||
fn base_data_mut(&mut self) -> &mut BaseModelData;
|
||||
|
||||
/// Set the ID for this model
|
||||
fn id(mut self, id: u32) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.base_data_mut().id = id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the model, updating the modified timestamp
|
||||
fn build(mut self) -> Self
|
||||
where
|
||||
@@ -95,9 +86,47 @@ pub trait Index {
|
||||
}
|
||||
|
||||
/// Base struct that all models should include
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
///
|
||||
/// # ID Management
|
||||
///
|
||||
/// The `id` field is managed automatically by OurDB when using incremental mode:
|
||||
///
|
||||
/// 1. When creating a new model, the `id` field is initialized to 0.
|
||||
/// 2. When saving the model to the database with `set()`, OurDB will:
|
||||
/// - If the ID is 0, auto-generate a new ID and update the model.
|
||||
/// - If the ID is not 0, use the provided ID for updates.
|
||||
///
|
||||
/// # Important Notes
|
||||
///
|
||||
/// - Never manually set the `id` field to a specific value. Always use 0 for new models.
|
||||
/// - The ID 0 is reserved for new models and should not be used for existing models.
|
||||
/// - After saving a model, use the ID returned by `set()` to retrieve the model from the database.
|
||||
/// - The original model passed to `set()` is not modified; you need to retrieve the updated model.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// // Create a new model with ID 0
|
||||
/// let user = User::new()
|
||||
/// .username("johndoe")
|
||||
/// .email("john.doe@example.com")
|
||||
/// .build();
|
||||
///
|
||||
/// // Save the model and get the assigned ID
|
||||
/// let user_id = db.collection().set(&user).expect("Failed to save user");
|
||||
///
|
||||
/// // Retrieve the model with the assigned ID
|
||||
/// let db_user = db.collection().get_by_id(user_id).expect("Failed to get user");
|
||||
/// ```
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
pub struct BaseModelData {
|
||||
/// Unique incremental ID per circle
|
||||
/// Unique incremental ID - will be auto-generated by OurDB
|
||||
///
|
||||
/// This field is automatically managed by OurDB:
|
||||
/// - For new models, set this to 0 and OurDB will auto-generate an ID.
|
||||
/// - For existing models, this will be the ID assigned by OurDB.
|
||||
///
|
||||
/// Do not manually set this field to a specific value.
|
||||
pub id: u32,
|
||||
|
||||
/// Unix epoch timestamp for creation time
|
||||
@@ -111,11 +140,12 @@ pub struct BaseModelData {
|
||||
}
|
||||
|
||||
impl BaseModelData {
|
||||
/// Create a new BaseModelData instance
|
||||
pub fn new(id: u32) -> Self {
|
||||
/// Create a new BaseModelData instance with ID set to 0
|
||||
/// The ID will be auto-generated by OurDB when the model is saved
|
||||
pub fn new() -> Self {
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
Self {
|
||||
id,
|
||||
id: 0, // This will be replaced by OurDB with an auto-generated ID
|
||||
created_at: now,
|
||||
modified_at: now,
|
||||
comments: Vec::new(),
|
||||
@@ -123,8 +153,8 @@ impl BaseModelData {
|
||||
}
|
||||
|
||||
/// Create a new BaseModelDataBuilder
|
||||
pub fn builder(id: u32) -> BaseModelDataBuilder {
|
||||
BaseModelDataBuilder::new(id)
|
||||
pub fn builder() -> BaseModelDataBuilder {
|
||||
BaseModelDataBuilder::new()
|
||||
}
|
||||
|
||||
/// Add a comment to this model
|
||||
@@ -143,11 +173,18 @@ impl BaseModelData {
|
||||
pub fn update_modified(&mut self) {
|
||||
self.modified_at = chrono::Utc::now().timestamp();
|
||||
}
|
||||
|
||||
/// Update the ID of this model
|
||||
pub fn update_id(&mut self, id: u32) {
|
||||
self.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
pub mod base_data_builder;
|
||||
pub use base_data_builder::BaseModelDataOps;
|
||||
|
||||
/// Builder for BaseModelData
|
||||
pub struct BaseModelDataBuilder {
|
||||
id: u32,
|
||||
created_at: Option<i64>,
|
||||
modified_at: Option<i64>,
|
||||
comments: Vec<u32>,
|
||||
@@ -155,9 +192,8 @@ pub struct BaseModelDataBuilder {
|
||||
|
||||
impl BaseModelDataBuilder {
|
||||
/// Create a new BaseModelDataBuilder
|
||||
pub fn new(id: u32) -> Self {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id,
|
||||
created_at: None,
|
||||
modified_at: None,
|
||||
comments: Vec::new(),
|
||||
@@ -192,7 +228,7 @@ impl BaseModelDataBuilder {
|
||||
pub fn build(self) -> BaseModelData {
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
BaseModelData {
|
||||
id: self.id,
|
||||
id: 0, // This will be replaced by OurDB with an auto-generated ID
|
||||
created_at: self.created_at.unwrap_or(now),
|
||||
modified_at: self.modified_at.unwrap_or(now),
|
||||
comments: self.comments,
|
||||
|
192
prompts/rhai_rs_generation_prompt.md
Normal file
192
prompts/rhai_rs_generation_prompt.md
Normal file
@@ -0,0 +1,192 @@
|
||||
## AI Prompt: Generate `rhai.rs` for a new Rust Model
|
||||
|
||||
**Objective:**
|
||||
Create a `rhai.rs` file to expose the Rust model `[YourModelName]` (and any related owned sub-models like `[YourSubModelName]`) to the Rhai scripting engine. This file should allow Rhai scripts to create, manipulate, and retrieve instances of `[YourModelName]`.
|
||||
|
||||
**Input Requirements (Provide this information for your specific model):**
|
||||
|
||||
1. **Rust Struct Definition(s):**
|
||||
```rust
|
||||
// Example for [YourModelName]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)] // Ensure necessary derives
|
||||
pub struct [YourModelName] {
|
||||
pub base_data: BaseModelData, // Common field for ID
|
||||
pub unique_id_field: String, // Example: like flow_uuid
|
||||
pub name: String,
|
||||
pub status: String,
|
||||
// If it owns a collection of sub-models:
|
||||
pub sub_items: Vec<[YourSubModelName]>,
|
||||
// Other fields...
|
||||
}
|
||||
|
||||
impl [YourModelName] {
|
||||
// Constructor
|
||||
pub fn new(id: u32, unique_id_field: String /*, other essential params */) -> Self {
|
||||
Self {
|
||||
base_data: BaseModelData::new(id),
|
||||
unique_id_field,
|
||||
name: "".to_string(), // Default or passed in
|
||||
status: "".to_string(), // Default or passed in
|
||||
sub_items: Vec::new(),
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// Builder methods
|
||||
pub fn name(mut self, name: String) -> Self {
|
||||
self.name = name;
|
||||
self
|
||||
}
|
||||
pub fn status(mut self, status: String) -> Self {
|
||||
self.status = status;
|
||||
self
|
||||
}
|
||||
// Method to add sub-items
|
||||
pub fn add_sub_item(mut self, item: [YourSubModelName]) -> Self {
|
||||
self.sub_items.push(item);
|
||||
self
|
||||
}
|
||||
// Other methods to expose...
|
||||
}
|
||||
|
||||
// Example for [YourSubModelName] (if applicable)
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct [YourSubModelName] {
|
||||
pub id: u32,
|
||||
pub description: String,
|
||||
// ...
|
||||
}
|
||||
|
||||
impl [YourSubModelName] {
|
||||
pub fn new(id: u32, description: String) -> Self {
|
||||
Self { id, description }
|
||||
}
|
||||
// Builder methods for sub-model...
|
||||
}
|
||||
```
|
||||
|
||||
2. **Key ID fields that need `i64` (Rhai) to `u32` (Rust) conversion:** (e.g., `base_data.id`, `[YourSubModelName].id`, any foreign key IDs).
|
||||
|
||||
**Implementation Guidelines for `rhai.rs`:**
|
||||
|
||||
1. **File Structure:**
|
||||
* Start with necessary imports: `rhai::{Dynamic, Engine, EvalAltResult, NativeCallContext, Position}`, `std::sync::Arc`, your model structs, `BaseModelData`, `OurDB`.
|
||||
* Include the `i64_to_u32` helper function.
|
||||
* Define `pub fn register_[your_model_name]_rhai_module(engine: &mut Engine, db: Arc<OurDB>) { ... }`.
|
||||
|
||||
2. **Helper Function for ID Conversion:**
|
||||
```rust
|
||||
fn i64_to_u32(val: i64, context_pos: Position, field_name: &str, object_name: &str) -> Result<u32, Box<EvalAltResult>> {
|
||||
val.try_into().map_err(|_e| {
|
||||
Box::new(EvalAltResult::ErrorArithmetic(
|
||||
format!("Conversion error for {} in {} from i64 to u32", field_name, object_name),
|
||||
context_pos,
|
||||
))
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
3. **Constructors (e.g., `new_[your_model_name]`):**
|
||||
* Use `NativeCallContext` for manual argument parsing and `i64_to_u32` conversion for ID fields.
|
||||
* Example:
|
||||
```rust
|
||||
engine.register_fn("new_[your_model_name]",
|
||||
move |context: NativeCallContext, id_i64: i64, unique_id_str: String /*, other_args... */|
|
||||
-> Result<[YourModelName], Box<EvalAltResult>> {
|
||||
let id_u32 = i64_to_u32(id_i64, context.position(), "id", "new_[your_model_name]")?;
|
||||
Ok([YourModelName]::new(id_u32, unique_id_str /*, ... */))
|
||||
});
|
||||
```
|
||||
* Do the same for `new_[your_sub_model_name]` if applicable.
|
||||
* **Note on `adapter_macros`**: `adapt_rhai_i64_input_fn!` is generally NOT suitable for constructors with multiple arguments or mixed types (e.g., `u32` and `String`). Prefer `NativeCallContext`.
|
||||
|
||||
4. **Builder Methods:**
|
||||
* Register functions that take ownership of the model, modify it, and return it.
|
||||
* Example:
|
||||
```rust
|
||||
engine.register_fn("name", |model: [YourModelName], name_val: String| -> [YourModelName] { model.name(name_val) });
|
||||
engine.register_fn("status", |model: [YourModelName], status_val: String| -> [YourModelName] { model.status(status_val) });
|
||||
// For adding sub-items (if applicable)
|
||||
engine.register_fn("add_sub_item", |model: [YourModelName], item: [YourSubModelName]| -> [YourModelName] { model.add_sub_item(item) });
|
||||
```
|
||||
|
||||
5. **Getters:**
|
||||
* Use `engine.register_get("field_name", |model: &mut [YourModelName]| -> Result<FieldType, Box<EvalAltResult>> { Ok(model.field.clone()) });`
|
||||
* For `base_data.id` (u32), cast to `i64` for Rhai: `Ok(model.base_data.id as i64)`
|
||||
* **For `Vec<[YourSubModelName]>` fields (e.g., `sub_items`):** Convert to `rhai::Array`.
|
||||
```rust
|
||||
engine.register_get("sub_items", |model: &mut [YourModelName]| -> Result<rhai::Array, Box<EvalAltResult>> {
|
||||
let rhai_array = model.sub_items.iter().cloned().map(Dynamic::from).collect::<rhai::Array>();
|
||||
Ok(rhai_array)
|
||||
});
|
||||
```
|
||||
(Ensure `[YourSubModelName]` is `Clone` and works with `Dynamic::from`).
|
||||
|
||||
6. **Setters (Less common if using a full builder pattern, but can be useful):**
|
||||
* Use `engine.register_set("field_name", |model: &mut [YourModelName], value: FieldType| { model.field = value; Ok(()) });`
|
||||
* If a setter method in Rust takes an ID (e.g., `set_related_id(id: u32)`), and you want to call it from Rhai with an `i64`:
|
||||
* You *could* use `adapter_macros::adapt_rhai_i64_input_method!(YourModelType::set_related_id, u32)` if `YourModelType::set_related_id` takes `&mut self, u32`.
|
||||
* Or, handle manually with `NativeCallContext` if the method signature is more complex or doesn't fit the macro.
|
||||
|
||||
7. **Other Custom Methods:**
|
||||
* Register any other public methods from your Rust struct that should be callable.
|
||||
* Example: `engine.register_fn("custom_method_name", |model: &mut [YourModelName]| model.custom_method_in_rust());`
|
||||
|
||||
8. **Database Interaction (Using actual `OurDB` methods like `set` and `get_by_id`):**
|
||||
* The `Arc<OurDB>` instance passed to `register_[your_model_name]_rhai_module` should be cloned and *captured* by the closures for DB functions.
|
||||
* The Rhai script will call these functions without explicitly passing the DB instance (e.g., `set_my_model(my_model_instance);`, `let m = get_my_model_by_id(123);`).
|
||||
* Ensure proper error handling, converting DB errors and `Option::None` (for getters) to `Box<EvalAltResult::ErrorRuntime(...)`.
|
||||
|
||||
* **Example for `set_[your_model_name]`:**
|
||||
```rust
|
||||
// In register_[your_model_name]_rhai_module(engine: &mut Engine, db: Arc<OurDB>)
|
||||
let captured_db_for_set = Arc::clone(&db);
|
||||
engine.register_fn("set_[your_model_name]",
|
||||
move |model: [YourModelName]| -> Result<(), Box<EvalAltResult>> {
|
||||
captured_db_for_set.set(&model).map_err(|e| {
|
||||
Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Failed to set [YourModelName] (ID: {}): {}", model.base_data.id, e).into(),
|
||||
Position::NONE,
|
||||
))
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
* **Example for `get_[your_model_name]_by_id`:**
|
||||
```rust
|
||||
// In register_[your_model_name]_rhai_module(engine: &mut Engine, db: Arc<OurDB>)
|
||||
let captured_db_for_get = Arc::clone(&db);
|
||||
engine.register_fn("get_[your_model_name]_by_id",
|
||||
move |context: NativeCallContext, id_i64: i64| -> Result<[YourModelName], Box<EvalAltResult>> {
|
||||
let id_u32 = i64_to_u32(id_i64, context.position(), "id", "get_[your_model_name]_by_id")?;
|
||||
|
||||
captured_db_for_get.get_by_id(id_u32) // Assumes OurDB directly implements Collection<_, [YourModelName]>
|
||||
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("Error getting [YourModelName] (ID: {}): {}", id_u32, e).into(),
|
||||
Position::NONE,
|
||||
)))?
|
||||
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(
|
||||
format!("[YourModelName] with ID {} not found", id_u32).into(),
|
||||
Position::NONE,
|
||||
)))
|
||||
});
|
||||
```
|
||||
|
||||
**Example Output Snippet (Illustrating a few parts):**
|
||||
|
||||
```rust
|
||||
// #[...]
|
||||
// pub struct MyItem { /* ... */ }
|
||||
// impl MyItem { /* ... */ }
|
||||
// pub fn register_my_item_rhai_module(engine: &mut Engine, db: Arc<OurDB>) {
|
||||
// fn i64_to_u32(...) { /* ... */ }
|
||||
//
|
||||
// engine.register_fn("new_my_item", |ctx: NativeCallContext, id: i64, name: String| { /* ... */ });
|
||||
// engine.register_fn("name", |item: MyItem, name: String| -> MyItem { /* ... */ });
|
||||
// engine.register_get("id", |item: &mut MyItem| Ok(item.base_data.id as i64));
|
||||
// engine.register_get("sub_elements", |item: &mut MyItem| {
|
||||
// Ok(item.sub_elements.iter().cloned().map(Dynamic::from).collect::<rhai::Array>())
|
||||
// });
|
||||
// // ... etc.
|
||||
// }
|
||||
```
|
290
rhai_client_macros/Cargo.lock
generated
Normal file
290
rhai_client_macros/Cargo.lock
generated
Normal file
@@ -0,0 +1,290 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"const-random",
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "const-random"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
||||
dependencies = [
|
||||
"const-random-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random-macro"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"once_cell",
|
||||
"tiny-keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.172"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
|
||||
|
||||
[[package]]
|
||||
name = "rhai"
|
||||
version = "1.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bitflags",
|
||||
"instant",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"rhai_codegen",
|
||||
"smallvec",
|
||||
"smartstring",
|
||||
"thin-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_client_macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rhai",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_codegen"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
|
||||
|
||||
[[package]]
|
||||
name = "smartstring"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"static_assertions",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thin-vec"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||
dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.14.2+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
|
||||
dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
14
rhai_client_macros/Cargo.toml
Normal file
14
rhai_client_macros/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "rhai_client_macros"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Procedural macros for generating Rhai client functions from Rust functions"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
syn = { version = "2.0", features = ["full", "extra-traits"] }
|
||||
rhai = "1.21.0"
|
348
rhai_client_macros/src/lib.rs
Normal file
348
rhai_client_macros/src/lib.rs
Normal file
@@ -0,0 +1,348 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::{quote, format_ident};
|
||||
use syn::{parse_macro_input, ItemFn, FnArg, Pat, PatType, ReturnType, parse_quote};
|
||||
|
||||
/// Procedural macro that generates a Rhai client function for a Rust function.
|
||||
///
|
||||
/// When applied to a Rust function, it generates a corresponding function with a '_rhai_client' suffix
|
||||
/// that calls the original function through the Rhai engine.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// #[rhai]
|
||||
/// fn hello(name: String) -> String {
|
||||
/// format!("Hello, {}!", name)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// This will generate:
|
||||
///
|
||||
/// ```rust
|
||||
/// fn hello_rhai_client(engine: &rhai::Engine, name: String) -> String {
|
||||
/// let script = format!("hello(\"{}\")", name.replace("\"", "\\\""));
|
||||
/// engine.eval::<String>(&script).unwrap_or_else(|err| {
|
||||
/// eprintln!("Rhai script error: {}", err);
|
||||
/// String::new()
|
||||
/// })
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Note: The macro handles type conversions between Rust and Rhai types,
|
||||
/// particularly for integer types (Rhai uses i64 internally).
|
||||
#[proc_macro_attribute]
|
||||
pub fn rhai(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
// Parse the input function
|
||||
let input_fn = parse_macro_input!(item as ItemFn);
|
||||
let fn_name = &input_fn.sig.ident;
|
||||
let fn_name_str = fn_name.to_string();
|
||||
|
||||
// Create the client function name (original + _rhai_client)
|
||||
let client_fn_name = format_ident!("{}_rhai_client", fn_name);
|
||||
|
||||
// Extract function parameters
|
||||
let mut param_names = Vec::new();
|
||||
let mut param_types = Vec::new();
|
||||
let mut param_declarations = Vec::new();
|
||||
|
||||
for arg in &input_fn.sig.inputs {
|
||||
match arg {
|
||||
FnArg::Typed(PatType { pat, ty, .. }) => {
|
||||
if let Pat::Ident(pat_ident) = &**pat {
|
||||
let param_name = &pat_ident.ident;
|
||||
param_names.push(param_name.clone());
|
||||
param_types.push(ty.clone());
|
||||
param_declarations.push(quote! { #param_name: #ty });
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Skip self parameters
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine return type
|
||||
let return_type = match &input_fn.sig.output {
|
||||
ReturnType::Default => parse_quote!(()),
|
||||
ReturnType::Type(_, ty) => ty.clone(),
|
||||
};
|
||||
|
||||
// Generate parameter formatting for the Rhai script
|
||||
let param_format_strings = param_names.iter().zip(param_types.iter()).map(|(name, ty)| {
|
||||
let type_str = quote! { #ty }.to_string();
|
||||
|
||||
// Handle different parameter types
|
||||
if type_str.contains("String") {
|
||||
quote! {
|
||||
format!("\"{}\"" , #name.replace("\"", "\\\""))
|
||||
}
|
||||
} else if type_str.contains("bool") {
|
||||
quote! {
|
||||
format!("{}", #name)
|
||||
}
|
||||
} else if type_str.contains("i32") || type_str.contains("u32") {
|
||||
// Convert smaller integer types to i64 for Rhai
|
||||
quote! {
|
||||
format!("{}", #name as i64)
|
||||
}
|
||||
} else if type_str.contains("i64") || type_str.contains("u64") || type_str.contains("f32") || type_str.contains("f64") {
|
||||
// Other numeric types
|
||||
quote! {
|
||||
format!("{}", #name)
|
||||
}
|
||||
} else {
|
||||
// For complex types, just pass the variable name
|
||||
// The Rhai engine will handle the conversion
|
||||
quote! {
|
||||
#name.to_string()
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Determine if the return type needs conversion
|
||||
let return_type_str = quote! { #return_type }.to_string();
|
||||
|
||||
// Generate the client function with appropriate type conversions
|
||||
let client_fn = if return_type_str.contains("i32") || return_type_str.contains("u32") {
|
||||
// For integer return types that need conversion
|
||||
quote! {
|
||||
fn #client_fn_name(engine: &rhai::Engine, #(#param_declarations),*) -> #return_type {
|
||||
let script = format!(
|
||||
"{}({})",
|
||||
#fn_name_str,
|
||||
&[#(#param_format_strings),*].join(", ")
|
||||
);
|
||||
|
||||
match engine.eval::<i64>(&script) {
|
||||
Ok(result) => result as #return_type,
|
||||
Err(err) => {
|
||||
eprintln!("Rhai script error: {}", err);
|
||||
0 as #return_type // Use 0 as default for numeric types
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if return_type_str.contains("String") {
|
||||
// For String return type
|
||||
quote! {
|
||||
fn #client_fn_name(engine: &rhai::Engine, #(#param_declarations),*) -> #return_type {
|
||||
let script = format!(
|
||||
"{}({})",
|
||||
#fn_name_str,
|
||||
&[#(#param_format_strings),*].join(", ")
|
||||
);
|
||||
|
||||
match engine.eval::<#return_type>(&script) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
eprintln!("Rhai script error: {}", err);
|
||||
String::new() // Empty string as default
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if return_type_str.contains("bool") {
|
||||
// For boolean return type
|
||||
quote! {
|
||||
fn #client_fn_name(engine: &rhai::Engine, #(#param_declarations),*) -> #return_type {
|
||||
let script = format!(
|
||||
"{}({})",
|
||||
#fn_name_str,
|
||||
&[#(#param_format_strings),*].join(", ")
|
||||
);
|
||||
|
||||
match engine.eval::<#return_type>(&script) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
eprintln!("Rhai script error: {}", err);
|
||||
false // False as default
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For complex types or other types
|
||||
quote! {
|
||||
fn #client_fn_name(engine: &rhai::Engine, #(#param_declarations),*) -> #return_type {
|
||||
let script = format!(
|
||||
"{}({})",
|
||||
#fn_name_str,
|
||||
&[#(#param_format_strings),*].join(", ")
|
||||
);
|
||||
|
||||
match engine.eval::<#return_type>(&script) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
eprintln!("Rhai script error: {}", err);
|
||||
panic!("Failed to evaluate Rhai script: {}", err) // Panic for complex types
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Combine the original function and the generated client function
|
||||
let output = quote! {
|
||||
#input_fn
|
||||
|
||||
#client_fn
|
||||
};
|
||||
|
||||
output.into()
|
||||
}
|
||||
|
||||
/// A more advanced version of the rhai macro that handles different parameter types better.
|
||||
///
|
||||
/// This version properly escapes strings and handles different parameter types more accurately.
|
||||
/// It's recommended to use this version for more complex functions.
|
||||
#[proc_macro_attribute]
|
||||
pub fn rhai_advanced(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
// Parse the input function
|
||||
let input_fn = parse_macro_input!(item as ItemFn);
|
||||
let fn_name = &input_fn.sig.ident;
|
||||
let fn_name_str = fn_name.to_string();
|
||||
|
||||
// Create the client function name (original + _rhai_client)
|
||||
let client_fn_name = format_ident!("{}_rhai_client", fn_name);
|
||||
|
||||
// Extract function parameters
|
||||
let mut param_names = Vec::new();
|
||||
let mut param_types = Vec::new();
|
||||
let mut param_declarations = Vec::new();
|
||||
|
||||
for arg in &input_fn.sig.inputs {
|
||||
match arg {
|
||||
FnArg::Typed(PatType { pat, ty, .. }) => {
|
||||
if let Pat::Ident(pat_ident) = &**pat {
|
||||
let param_name = &pat_ident.ident;
|
||||
param_names.push(param_name.clone());
|
||||
param_types.push(ty.clone());
|
||||
param_declarations.push(quote! { #param_name: #ty });
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Skip self parameters
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Determine return type
|
||||
let return_type = match &input_fn.sig.output {
|
||||
ReturnType::Default => parse_quote!(()),
|
||||
ReturnType::Type(_, ty) => ty.clone(),
|
||||
};
|
||||
|
||||
// Generate parameter formatting for the Rhai script
|
||||
let param_format_expressions = param_names.iter().zip(param_types.iter()).map(|(name, ty)| {
|
||||
let type_str = quote! { #ty }.to_string();
|
||||
|
||||
// Handle different parameter types
|
||||
if type_str.contains("String") {
|
||||
quote! {
|
||||
format!("\"{}\"", #name.replace("\"", "\\\""))
|
||||
}
|
||||
} else if type_str.contains("bool") {
|
||||
quote! {
|
||||
format!("{}", #name)
|
||||
}
|
||||
} else if type_str.contains("i32") || type_str.contains("u32") {
|
||||
// Convert smaller integer types to i64 for Rhai
|
||||
quote! {
|
||||
format!("{}", #name as i64)
|
||||
}
|
||||
} else if type_str.contains("i") || type_str.contains("u") || type_str.contains("f") {
|
||||
// Other numeric types
|
||||
quote! {
|
||||
format!("{}", #name)
|
||||
}
|
||||
} else {
|
||||
// Default for other types
|
||||
quote! {
|
||||
format!("{:?}", #name)
|
||||
}
|
||||
}
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
// Determine if the return type needs conversion
|
||||
let return_type_str = quote! { #return_type }.to_string();
|
||||
let needs_return_conversion = return_type_str.contains("i32") || return_type_str.contains("u32");
|
||||
|
||||
// Generate the client function with appropriate type conversions
|
||||
let client_fn = if needs_return_conversion {
|
||||
quote! {
|
||||
fn #client_fn_name(engine: &rhai::Engine, #(#param_declarations),*) -> #return_type {
|
||||
let script = format!(
|
||||
"{}({})",
|
||||
#fn_name_str,
|
||||
&[#(#param_format_expressions),*].join(", ")
|
||||
);
|
||||
|
||||
match engine.eval::<i64>(&script) {
|
||||
Ok(result) => result as #return_type,
|
||||
Err(err) => {
|
||||
eprintln!("Rhai script error: {}", err);
|
||||
#return_type::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
fn #client_fn_name(engine: &rhai::Engine, #(#param_declarations),*) -> #return_type {
|
||||
let script = format!(
|
||||
"{}({})",
|
||||
#fn_name_str,
|
||||
&[#(#param_format_expressions),*].join(", ")
|
||||
);
|
||||
|
||||
match engine.eval::<#return_type>(&script) {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
eprintln!("Rhai script error: {}", err);
|
||||
#return_type::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Combine the original function and the generated client function
|
||||
let output = quote! {
|
||||
#input_fn
|
||||
|
||||
#client_fn
|
||||
};
|
||||
|
||||
output.into()
|
||||
}
|
||||
|
||||
/// Macro that generates a module with Rhai client functions for all functions in scope.
|
||||
///
|
||||
/// This macro should be used at the module level to generate Rhai client functions for all
|
||||
/// functions marked with the #[rhai] attribute.
|
||||
#[proc_macro]
|
||||
pub fn generate_rhai_module(_item: TokenStream) -> TokenStream {
|
||||
// This would be a more complex implementation that would need to
|
||||
// scan the module for functions marked with #[rhai] and generate
|
||||
// client functions for all of them.
|
||||
//
|
||||
// For simplicity, we'll just return a placeholder implementation
|
||||
|
||||
let output = quote! {
|
||||
/// Register all functions marked with #[rhai] in this module with the Rhai engine.
|
||||
///
|
||||
/// This function handles type conversions between Rust and Rhai types automatically.
|
||||
/// For example, it converts between Rust's i32 and Rhai's i64 types.
|
||||
pub fn register_rhai_functions(engine: &mut rhai::Engine) {
|
||||
// This would be generated based on the functions in the module
|
||||
println!("Registering Rhai functions...");
|
||||
|
||||
// In a real implementation, this would iterate through all functions
|
||||
// marked with #[rhai] and register them with the engine.
|
||||
}
|
||||
};
|
||||
|
||||
output.into()
|
||||
}
|
@@ -22,9 +22,9 @@ pub enum BusinessType {
|
||||
|
||||
// Company represents a company registered in the Freezone
|
||||
pub struct Company {
|
||||
base.Base // Base struct for common fields
|
||||
base.Base // Provides id u32, creation_time, mod_time, comments []u32
|
||||
pub mut:
|
||||
id u32
|
||||
// id u32 is provided by base.Base
|
||||
name string
|
||||
registration_number string
|
||||
incorporation_date ourtime.OurTime
|
||||
@@ -37,7 +37,7 @@ pub mut:
|
||||
industry string
|
||||
description string
|
||||
status CompanyStatus
|
||||
created_at ourtime.OurTime
|
||||
updated_at ourtime.OurTime
|
||||
// created_at is provided by base.Base.creation_time
|
||||
// updated_at is provided by base.Base.mod_time
|
||||
shareholders []Shareholder
|
||||
}
|
||||
|
@@ -1,58 +0,0 @@
|
||||
module biz
|
||||
import base
|
||||
|
||||
import freeflowuniverse.herolib.data.ourtime
|
||||
|
||||
|
||||
// MeetingStatus represents the status of a meeting
|
||||
pub enum MeetingStatus {
|
||||
scheduled
|
||||
completed
|
||||
cancelled
|
||||
}
|
||||
|
||||
// AttendeeRole represents the role of an attendee in a meeting
|
||||
pub enum AttendeeRole {
|
||||
coordinator
|
||||
member
|
||||
secretary
|
||||
participant
|
||||
advisor
|
||||
admin
|
||||
}
|
||||
|
||||
// AttendeeStatus represents the status of an attendee's participation
|
||||
pub enum AttendeeStatus {
|
||||
confirmed
|
||||
pending
|
||||
declined
|
||||
}
|
||||
|
||||
// Meeting represents a board meeting of a company or other meeting
|
||||
pub struct Meeting {
|
||||
base.Base // Base struct for common fields
|
||||
pub mut:
|
||||
id u32
|
||||
company_id u32
|
||||
title string
|
||||
date ourtime.OurTime
|
||||
location string
|
||||
description string
|
||||
status MeetingStatus
|
||||
minutes string
|
||||
created_at ourtime.OurTime
|
||||
updated_at ourtime.OurTime
|
||||
attendees []Attendee
|
||||
}
|
||||
|
||||
// Attendee represents an attendee of a board meeting
|
||||
pub struct Attendee {
|
||||
pub mut:
|
||||
id u32
|
||||
meeting_id u32
|
||||
user_id u32
|
||||
name string
|
||||
role AttendeeRole
|
||||
status AttendeeStatus
|
||||
created_at ourtime.OurTime
|
||||
}
|
@@ -18,32 +18,27 @@ pub enum ProductStatus {
|
||||
unavailable
|
||||
}
|
||||
|
||||
// ProductComponent represents a component of a product
|
||||
// ProductComponent represents a component or sub-part of a product.
|
||||
// Its lifecycle is tied to the parent Product and it does not have its own independent ID.
|
||||
pub struct ProductComponent {
|
||||
pub mut:
|
||||
id u32
|
||||
name string
|
||||
description string
|
||||
quantity int
|
||||
created_at ourtime.OurTime
|
||||
updated_at ourtime.OurTime
|
||||
}
|
||||
|
||||
// Product represents a product or service offered by the Freezone
|
||||
// Product represents a product or service offered
|
||||
pub struct Product {
|
||||
base.Base // Base struct for common fields
|
||||
base.Base // Provides id u32, creation_time, mod_time, comments []u32
|
||||
pub mut:
|
||||
id u32
|
||||
name string
|
||||
description string
|
||||
price currency.Currency
|
||||
type_ ProductType
|
||||
category string
|
||||
status ProductStatus
|
||||
created_at ourtime.OurTime
|
||||
updated_at ourtime.OurTime
|
||||
max_amount u16 // means allows us to define how many max of this there are
|
||||
purchase_till ourtime.OurTime
|
||||
active_till ourtime.OurTime // after this product no longer active if e.g. a service
|
||||
components []ProductComponent
|
||||
max_amount u16 // Maximum available quantity of this product, if applicable
|
||||
purchase_till ourtime.OurTime // Date until which this product can be purchased
|
||||
active_till ourtime.OurTime // Date until which this product/service remains active (e.g., for subscriptions)
|
||||
components []ProductComponent // List of components that make up this product
|
||||
}
|
||||
|
@@ -14,28 +14,30 @@ pub enum SaleStatus {
|
||||
|
||||
// Sale represents a sale of products or services
|
||||
pub struct Sale {
|
||||
base.Base // Base struct for common fields
|
||||
base.Base // Provides id u32, creation_time, mod_time, comments []u32
|
||||
pub mut:
|
||||
id u32
|
||||
company_id u32
|
||||
// id u32 is provided by base.Base
|
||||
company_id u32 // Reference to Company.id that made the sale
|
||||
buyer_name string
|
||||
buyer_email string
|
||||
total_amount currency.Currency
|
||||
status SaleStatus
|
||||
sale_date ourtime.OurTime
|
||||
created_at ourtime.OurTime
|
||||
updated_at ourtime.OurTime
|
||||
// created_at is provided by base.Base.creation_time
|
||||
// updated_at is provided by base.Base.mod_time
|
||||
items []SaleItem
|
||||
}
|
||||
|
||||
// SaleItem represents an individual item within a Sale.
|
||||
// Its lifecycle is tied to the parent Sale.
|
||||
pub struct SaleItem {
|
||||
pub mut:
|
||||
id u32
|
||||
sale_id u32
|
||||
product_id u32
|
||||
name string
|
||||
// id u32 - Removed, component of Sale
|
||||
// sale_id u32 - Removed, implicit link to parent Sale
|
||||
product_id u32 // Reference to Product.id
|
||||
name string // Denormalized product name at time of sale
|
||||
quantity int
|
||||
unit_price currency.Currency
|
||||
unit_price currency.Currency // Price per unit at time of sale
|
||||
subtotal currency.Currency
|
||||
active_till ourtime.OurTime // after this product no longer active if e.g. a service
|
||||
service_active_until ourtime.OurTime? // Optional: For services, date until this specific purchased instance is active
|
||||
}
|
||||
|
@@ -12,16 +12,16 @@ pub enum ShareholderType {
|
||||
|
||||
// Shareholder represents a shareholder of a company
|
||||
pub struct Shareholder {
|
||||
base.Base // Base struct for common fields
|
||||
base.Base // Provides id u32, creation_time, mod_time, comments []u32
|
||||
pub mut:
|
||||
id u32
|
||||
company_id u32
|
||||
user_id u32
|
||||
name string
|
||||
shares f64
|
||||
percentage f64
|
||||
// id u32 is provided by base.Base
|
||||
company_id u32 // Reference to Company.id
|
||||
user_id u32 // Reference to User.id (if individual) or another entity ID (if corporate)
|
||||
name string // Denormalized name of the shareholder (user or corporate entity)
|
||||
shares f64 // Number of shares held
|
||||
percentage f64 // Percentage of ownership
|
||||
type_ ShareholderType
|
||||
since ourtime.OurTime
|
||||
created_at ourtime.OurTime
|
||||
updated_at ourtime.OurTime
|
||||
since ourtime.OurTime // Date since becoming a shareholder
|
||||
// created_at is provided by base.Base.creation_time
|
||||
// updated_at is provided by base.Base.mod_time
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user