sausage-plugin part 1: run, #[rpc], #[schema], rpc.discover, calls, locks, Ctx, hooks
Checkpoint. Known issues, fixed in part 2: short writes on stdout, byte-wise stdin reads, Result suffix on all result schemas, lost enum variant docs, serde errors as invalid params, missing item docs.
This commit is contained in:
+5
-1
@@ -1,4 +1,8 @@
|
||||
/target
|
||||
target/
|
||||
/.cargo-home
|
||||
/world
|
||||
/result
|
||||
|
||||
# Lock files of test and example crates: they resolve against the workspace lock.
|
||||
**/tests/**/Cargo.lock
|
||||
/examples/*/Cargo.lock
|
||||
|
||||
Generated
+262
-90
@@ -85,6 +85,12 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3bc62ac97cc33321f50863d514c3bc38a453947a8f9e781137e47c7401020aed"
|
||||
|
||||
[[package]]
|
||||
name = "async-task"
|
||||
version = "4.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.92"
|
||||
@@ -205,6 +211,18 @@ dependencies = [
|
||||
"winx",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cap-std"
|
||||
version = "4.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1ec78e242cfa2cfe276807ac2ecc00315a6c97786977414bcd1c3963b6c91b8"
|
||||
dependencies = [
|
||||
"cap-primitives",
|
||||
"io-extras",
|
||||
"io-lifetimes 3.0.1",
|
||||
"rustix",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.8.0"
|
||||
@@ -623,6 +641,27 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-streaming-iterator"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
|
||||
dependencies = [
|
||||
"instant",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.5.0"
|
||||
@@ -673,15 +712,6 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.34"
|
||||
@@ -690,6 +720,7 @@ checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
@@ -712,12 +743,49 @@ version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
|
||||
dependencies = [
|
||||
"fastrand 1.9.0",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"memchr",
|
||||
"parking",
|
||||
"pin-project-lite",
|
||||
"waker-fn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.34"
|
||||
@@ -739,6 +807,7 @@ dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
@@ -805,6 +874,12 @@ dependencies = [
|
||||
"stable_deref_trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.20"
|
||||
@@ -829,6 +904,9 @@ name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
@@ -841,6 +919,15 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f"
|
||||
dependencies = [
|
||||
"hashbrown 0.16.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
@@ -1056,23 +1143,12 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.11.5"
|
||||
name = "instant"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592"
|
||||
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c033f80b2c113cdf91ab7a33faa9cbc014726dcad99880c8609af2a370edf37d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1165,26 +1241,6 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d763e5b24120b4ddf50de6c92308156765aabfbbccebf401da7cff2d70a41ea"
|
||||
dependencies = [
|
||||
"kqueue-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue-sys"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leb128"
|
||||
version = "0.2.7"
|
||||
@@ -1218,6 +1274,17 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95b4103cffefa72eb8428cb6b47d6627161e51c2739fc5e3b734584157bc642a"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
@@ -1279,7 +1346,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
@@ -1290,33 +1356,6 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d"
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "8.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio",
|
||||
"notify-types",
|
||||
"walkdir",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify-types"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
@@ -1350,6 +1389,12 @@ version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
@@ -1625,6 +1670,31 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rsqlite-vfs"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c"
|
||||
dependencies = [
|
||||
"hashbrown 0.16.1",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.38.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
"libsqlite3-sys",
|
||||
"smallvec",
|
||||
"sqlite-wasm-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.28"
|
||||
@@ -1713,15 +1783,6 @@ dependencies = [
|
||||
"wait-timeout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sausage"
|
||||
version = "0.1.0"
|
||||
@@ -1732,11 +1793,13 @@ dependencies = [
|
||||
"http",
|
||||
"http-body-util",
|
||||
"include_dir",
|
||||
"notify",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tokio",
|
||||
"toml_edit",
|
||||
"uuid",
|
||||
"wasmtime",
|
||||
"wasmtime-wasi",
|
||||
"wasmtime-wasi-http",
|
||||
@@ -1756,16 +1819,39 @@ dependencies = [
|
||||
name = "sausage-plugin"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"futures",
|
||||
"sausage-interface",
|
||||
"sausage-plugin-macros",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"trybuild",
|
||||
"wstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sausage-plugin-macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sausage-vfs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"bytes",
|
||||
"cap-std",
|
||||
"proptest",
|
||||
"rusqlite",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"wasmtime",
|
||||
"wasmtime-wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
@@ -1929,6 +2015,18 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlite-wasm-rs"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"js-sys",
|
||||
"rsqlite-vfs",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.1"
|
||||
@@ -1986,13 +2084,19 @@ version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
||||
|
||||
[[package]]
|
||||
name = "target-tuple"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "876fef147edbcbddc8ac5cbbba92c7b86519e314e86638596c09673b2ed01e7f"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"fastrand 2.5.0",
|
||||
"getrandom 0.4.3",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
@@ -2199,6 +2303,21 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "trybuild"
|
||||
version = "1.0.122"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62db9c92d704393fbf2132041720cc80b689f2d3f28521015c2ac866223c11b8"
|
||||
dependencies = [
|
||||
"glob",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"target-tuple",
|
||||
"termcolor",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
@@ -2259,10 +2378,17 @@ version = "1.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cc1186384beb7dd8eedea376413fd654937285ea6c9cfbb928dc3043ea4b606"
|
||||
dependencies = [
|
||||
"getrandom 0.4.3",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
@@ -2279,14 +2405,10 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
name = "waker-fn"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7"
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
@@ -2303,6 +2425,15 @@ version = "0.11.1+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.4+wasi-0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.129"
|
||||
@@ -3033,6 +3164,15 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.258.3"
|
||||
@@ -3089,6 +3229,38 @@ version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
|
||||
|
||||
[[package]]
|
||||
name = "wstd"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29b52936db10a79bb724dadd1c2c3aac958e8229dcb1f1c7f2b7044ca9fc6a3a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-task",
|
||||
"bytes",
|
||||
"futures-lite",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"itoa",
|
||||
"pin-project-lite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"slab",
|
||||
"wasip2",
|
||||
"wstd-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wstd-macro"
|
||||
version = "0.6.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "153db9b65508bf6c2efe26617169840c03671ba5719a35868db7682a5261f7d4"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.119",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.3"
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
//! Shared access to OpenRPC documents and their Sausage extensions.
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// An OpenRPC document, including JSON Schema and extension fields.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Document(pub Value);
|
||||
|
||||
impl Document {
|
||||
/// Create an empty main interface.
|
||||
pub fn new(title: &str, version: &str) -> Self {
|
||||
Self(
|
||||
json!({"openrpc":"1.2.6", "info":{"title":title,"version":version},
|
||||
"methods":[], "components":{"schemas":{}},
|
||||
"x-sausage-endpoints":{}, "x-sausage-imports":{}}),
|
||||
)
|
||||
}
|
||||
|
||||
/// Read the method descriptions.
|
||||
pub fn methods(&self) -> &[Value] {
|
||||
self.0["methods"]
|
||||
.as_array()
|
||||
.map(Vec::as_slice)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Read a named schema.
|
||||
pub fn schema(&self, name: &str) -> Option<&Value> {
|
||||
self.0.get("components")?.get("schemas")?.get(name)
|
||||
}
|
||||
|
||||
/// Consume the document as a JSON value.
|
||||
pub fn into_value(self) -> Value {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
@@ -2,3 +2,5 @@
|
||||
//! (`compat`), and the generator of client code for `import!`.
|
||||
|
||||
pub mod compat;
|
||||
pub mod document;
|
||||
pub use document::Document;
|
||||
|
||||
@@ -6,3 +6,14 @@ description = "SDK for Sausage plugins."
|
||||
|
||||
[dependencies]
|
||||
sausage-plugin-macros = { version = "0.1", path = "macros" }
|
||||
sausage-interface = { path = "../interface" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
schemars = "1"
|
||||
futures = "0.3"
|
||||
|
||||
[target.'cfg(target_os = "wasi")'.dependencies]
|
||||
wstd = "0.6"
|
||||
|
||||
[dev-dependencies]
|
||||
trybuild = "1"
|
||||
|
||||
@@ -8,3 +8,6 @@ description = "Macros of the Sausage plugin SDK. Use them through `sausage-plugi
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "2", features = ["full"] }
|
||||
quote = "1"
|
||||
proc-macro2 = "1"
|
||||
|
||||
+251
-1
@@ -1 +1,251 @@
|
||||
//! Macros of the Sausage plugin SDK. Use them through `sausage-plugin`.
|
||||
//! Attribute macros for documented schemas and plugin dispatch.
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{
|
||||
Attribute, Fields, FnArg, ImplItem, Item, ItemImpl, ReturnType, Type, parse_macro_input,
|
||||
spanned::Spanned,
|
||||
};
|
||||
fn docs(attrs: &[Attribute]) -> String {
|
||||
attrs
|
||||
.iter()
|
||||
.filter_map(|a| {
|
||||
if a.path().is_ident("doc")
|
||||
&& let syn::Meta::NameValue(n) = &a.meta
|
||||
&& let syn::Expr::Lit(e) = &n.value
|
||||
&& let syn::Lit::Str(s) = &e.lit
|
||||
{
|
||||
return Some(s.value().trim().to_string());
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
fn documented(attrs: &[Attribute], span: proc_macro2::Span) -> syn::Result<()> {
|
||||
if docs(attrs).is_empty() {
|
||||
Err(syn::Error::new(span, "a doc comment is required"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
fn serde_subset(attrs: &[Attribute]) -> syn::Result<()> {
|
||||
for a in attrs {
|
||||
if a.path().is_ident("serde") {
|
||||
a.parse_nested_meta(|m| {
|
||||
if !["rename", "rename_all", "default", "skip_serializing_if"]
|
||||
.iter()
|
||||
.any(|s| m.path.is_ident(s))
|
||||
{
|
||||
return Err(m.error("serde attribute is outside the schema subset"));
|
||||
}
|
||||
if m.input.peek(syn::Token![=]) {
|
||||
let _: syn::Expr = m.value()?.parse()?;
|
||||
}
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
#[proc_macro_attribute]
|
||||
pub fn schema(_: TokenStream, input: TokenStream) -> TokenStream {
|
||||
let item = parse_macro_input!(input as Item);
|
||||
let result = (|| -> syn::Result<proc_macro2::TokenStream> {
|
||||
let (name, attrs, fields, is_params) = match &item {
|
||||
Item::Struct(s) => {
|
||||
if !matches!(s.fields, Fields::Named(_)) {
|
||||
return Err(syn::Error::new(
|
||||
s.span(),
|
||||
"schema structs require named fields",
|
||||
));
|
||||
}
|
||||
(
|
||||
&s.ident,
|
||||
&s.attrs,
|
||||
s.fields.iter().collect::<Vec<_>>(),
|
||||
true,
|
||||
)
|
||||
}
|
||||
Item::Enum(e) => {
|
||||
for v in &e.variants {
|
||||
if !matches!(v.fields, Fields::Unit) {
|
||||
return Err(syn::Error::new(
|
||||
v.span(),
|
||||
"schema enums require unit variants",
|
||||
));
|
||||
}
|
||||
serde_subset(&v.attrs)?;
|
||||
}
|
||||
(&e.ident, &e.attrs, vec![], false)
|
||||
}
|
||||
_ => {
|
||||
return Err(syn::Error::new(
|
||||
item.span(),
|
||||
"schema requires a struct or unit enum",
|
||||
));
|
||||
}
|
||||
};
|
||||
documented(attrs, name.span())?;
|
||||
serde_subset(attrs)?;
|
||||
let mut types = vec![];
|
||||
for f in fields {
|
||||
documented(&f.attrs, f.span())?;
|
||||
serde_subset(&f.attrs)?;
|
||||
types.push(&f.ty);
|
||||
}
|
||||
let params = if is_params {
|
||||
quote!(impl ::sausage_plugin::Params for #name {})
|
||||
} else {
|
||||
quote!()
|
||||
};
|
||||
Ok(quote! {
|
||||
#[derive(::sausage_plugin::serde::Serialize,::sausage_plugin::serde::Deserialize,::sausage_plugin::schemars::JsonSchema)]
|
||||
#[serde(crate="::sausage_plugin::serde")]
|
||||
#[schemars(crate="::sausage_plugin::schemars")]
|
||||
#item
|
||||
impl ::sausage_plugin::SchemaType for #name {}
|
||||
#params
|
||||
const _: () = { fn check() { fn supported<T: ::sausage_plugin::SchemaType>() {} #(supported::<#types>();)* } };
|
||||
})
|
||||
})();
|
||||
result.unwrap_or_else(syn::Error::into_compile_error).into()
|
||||
}
|
||||
#[proc_macro_attribute]
|
||||
pub fn rpc(args: TokenStream, input: TokenStream) -> TokenStream {
|
||||
let mut item = parse_macro_input!(input as ItemImpl);
|
||||
let args = proc_macro2::TokenStream::from(args);
|
||||
let result = (|| -> syn::Result<proc_macro2::TokenStream> {
|
||||
let mut grace = None;
|
||||
let parser = syn::meta::parser(|m| {
|
||||
if m.path.is_ident("grace_seconds") {
|
||||
grace = Some(m.value()?.parse::<syn::LitInt>()?);
|
||||
Ok(())
|
||||
} else if m.path.is_ident("endpoints") {
|
||||
let content;
|
||||
syn::parenthesized!(content in m.input);
|
||||
let _ = content.parse::<proc_macro2::TokenStream>()?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(m.error("unknown RPC option"))
|
||||
}
|
||||
});
|
||||
syn::parse::Parser::parse2(parser, args)?;
|
||||
let ty = &item.self_ty;
|
||||
let mut arms = vec![];
|
||||
let mut entries = vec![];
|
||||
let mut stop = quote!();
|
||||
for member in &mut item.items {
|
||||
let ImplItem::Fn(f) = member else { continue };
|
||||
let retired = f.attrs.iter().any(|a| {
|
||||
a.path()
|
||||
.segments
|
||||
.last()
|
||||
.is_some_and(|s| s.ident == "on_retired")
|
||||
});
|
||||
let stopping = f.attrs.iter().any(|a| {
|
||||
a.path()
|
||||
.segments
|
||||
.last()
|
||||
.is_some_and(|s| s.ident == "on_stop")
|
||||
});
|
||||
f.attrs.retain(|a| {
|
||||
!a.path()
|
||||
.segments
|
||||
.last()
|
||||
.is_some_and(|s| s.ident == "on_retired" || s.ident == "on_stop")
|
||||
});
|
||||
documented(&f.attrs, f.span())?;
|
||||
if f.sig.inputs.is_empty()
|
||||
|| !matches!(f.sig.inputs.first(),Some(FnArg::Receiver(r)) if r.reference.is_some() && r.mutability.is_none())
|
||||
{
|
||||
return Err(syn::Error::new(f.sig.span(), "RPC methods require &self"));
|
||||
}
|
||||
let name = &f.sig.ident;
|
||||
let mut args = vec![];
|
||||
let mut param = None;
|
||||
for arg in f.sig.inputs.iter().skip(1) {
|
||||
let FnArg::Typed(p) = arg else { unreachable!() };
|
||||
let is_ctx = matches!(p.ty.as_ref(),Type::Reference(r) if matches!(r.elem.as_ref(),Type::Path(t) if t.path.segments.last().is_some_and(|s|s.ident=="Ctx")));
|
||||
if is_ctx {
|
||||
if !args.is_empty() {
|
||||
return Err(syn::Error::new(p.span(), "ctx must follow &self"));
|
||||
}
|
||||
args.push(quote!(ctx));
|
||||
} else {
|
||||
if param.is_some() {
|
||||
return Err(syn::Error::new(
|
||||
p.span(),
|
||||
"RPC methods take at most one params struct",
|
||||
));
|
||||
}
|
||||
param = Some(p.ty.as_ref());
|
||||
args.push(quote!(::sausage_plugin::serde_json::from_value(params)?));
|
||||
}
|
||||
}
|
||||
let await_ = f.sig.asyncness.map(|_| quote!(.await));
|
||||
if stopping {
|
||||
if !args.is_empty() {
|
||||
return Err(syn::Error::new(f.span(), "stop hook takes only &self"));
|
||||
}
|
||||
stop = quote!(fn stop(&self)->::std::pin::Pin<Box<dyn ::std::future::Future<Output=()> + '_>> {Box::pin(async move {let _=self.#name() #await_;})});
|
||||
continue;
|
||||
}
|
||||
let wire = if retired {
|
||||
"rpc.retired".to_string()
|
||||
} else {
|
||||
name.to_string()
|
||||
};
|
||||
let p = param
|
||||
.map(|t| quote!(#t))
|
||||
.unwrap_or(quote!(::sausage_plugin::Empty));
|
||||
let output = match &f.sig.output {
|
||||
ReturnType::Type(_, t) => match t.as_ref() {
|
||||
Type::Path(t)
|
||||
if t.path.segments.last().is_some_and(|s| s.ident == "Result") =>
|
||||
{
|
||||
let syn::PathArguments::AngleBracketed(a) =
|
||||
&t.path.segments.last().unwrap().arguments
|
||||
else {
|
||||
return Err(syn::Error::new(t.span(), "expected Result<T>"));
|
||||
};
|
||||
let Some(syn::GenericArgument::Type(t)) = a.args.first() else {
|
||||
return Err(syn::Error::new(a.span(), "expected Result<T>"));
|
||||
};
|
||||
quote!(#t)
|
||||
}
|
||||
_ => return Err(syn::Error::new(t.span(), "RPC methods return Result<T>")),
|
||||
},
|
||||
_ => return Err(syn::Error::new(f.span(), "RPC methods return Result<T>")),
|
||||
};
|
||||
let doc = docs(&f.attrs);
|
||||
arms.push(quote!(#wire => { fn params_type<T: ::sausage_plugin::Params>() {} params_type::<#p>(); let result=self.#name(#(#args),*) #await_ ?; Ok(::sausage_plugin::serde_json::to_value(result)?) }));
|
||||
if !retired {
|
||||
entries.push(quote!(::sausage_plugin::method::<#p,#output>(&mut doc,#wire,#doc);));
|
||||
}
|
||||
}
|
||||
let grace =
|
||||
grace.map(|n| quote!(doc["x-sausage-grace"]=::sausage_plugin::serde_json::json!(#n);));
|
||||
Ok(quote! {#item impl ::sausage_plugin::Interface for #ty {
|
||||
fn discover(&self)->::sausage_plugin::serde_json::Value {let mut doc=::sausage_plugin::document(env!("CARGO_PKG_NAME"),env!("CARGO_PKG_VERSION")); #(#entries)* #grace doc}
|
||||
fn dispatch<'a>(&'a self, method:&'a str, params: ::sausage_plugin::serde_json::Value,ctx:&'a ::sausage_plugin::Ctx)->::std::pin::Pin<Box<dyn ::std::future::Future<Output=::sausage_plugin::Result<::sausage_plugin::serde_json::Value>>+'a>> {Box::pin(async move {if !params.is_object() {return Err(::sausage_plugin::Error::new(-32602,"params must be an object"));} match method {#(#arms,)* _=>Err(::sausage_plugin::Error::new(-32601,"method not found"))}})}
|
||||
#stop
|
||||
}})
|
||||
})();
|
||||
result.unwrap_or_else(syn::Error::into_compile_error).into()
|
||||
}
|
||||
#[proc_macro_attribute]
|
||||
pub fn on_retired(_: TokenStream, input: TokenStream) -> TokenStream {
|
||||
input
|
||||
}
|
||||
#[proc_macro_attribute]
|
||||
pub fn on_stop(_: TokenStream, input: TokenStream) -> TokenStream {
|
||||
input
|
||||
}
|
||||
#[proc_macro_attribute]
|
||||
pub fn test(_: TokenStream, input: TokenStream) -> TokenStream {
|
||||
let mut f = parse_macro_input!(input as syn::ItemFn);
|
||||
f.sig.asyncness = None;
|
||||
let body = f.block;
|
||||
f.block = Box::new(syn::parse_quote!({::sausage_plugin::test_run(async #body)}));
|
||||
quote!(#[::core::prelude::v1::test] #f).into()
|
||||
}
|
||||
|
||||
+633
-1
@@ -1 +1,633 @@
|
||||
//! SDK for Sausage plugins.
|
||||
//! JSON-RPC transport, lifetime, and helpers for single-threaded Sausage plugins.
|
||||
use futures::{channel::oneshot, future::LocalBoxFuture};
|
||||
pub use sausage_plugin_macros::{on_retired, on_stop, rpc, schema, test};
|
||||
use serde_json::{Value, json};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::HashMap,
|
||||
future::{Future, IntoFuture},
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
pub use {schemars, serde, serde_json};
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// A JSON-RPC failure.
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Error {
|
||||
pub code: i64,
|
||||
pub message: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub data: Option<Value>,
|
||||
}
|
||||
impl Error {
|
||||
pub fn new(code: i64, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
code,
|
||||
message: message.into(),
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&self.message)
|
||||
}
|
||||
}
|
||||
impl std::error::Error for Error {}
|
||||
impl From<std::io::Error> for Error {
|
||||
fn from(e: std::io::Error) -> Self {
|
||||
Self::new(-32603, e.to_string())
|
||||
}
|
||||
}
|
||||
impl From<serde_json::Error> for Error {
|
||||
fn from(e: serde_json::Error) -> Self {
|
||||
Self::new(-32602, e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Types within the supported schema subset.
|
||||
#[diagnostic::on_unimplemented(message = "this type is outside the Sausage schema subset")]
|
||||
pub trait SchemaType: schemars::JsonSchema {}
|
||||
/// Named parameter objects.
|
||||
#[diagnostic::on_unimplemented(message = "RPC params must be a #[schema] struct with named fields")]
|
||||
pub trait Params: SchemaType {}
|
||||
macro_rules! primitive { ($($t:ty),*) => { $(impl SchemaType for $t {})* }; }
|
||||
primitive!(
|
||||
String,
|
||||
bool,
|
||||
u8,
|
||||
u16,
|
||||
u32,
|
||||
u64,
|
||||
usize,
|
||||
i8,
|
||||
i16,
|
||||
i32,
|
||||
i64,
|
||||
isize,
|
||||
f32,
|
||||
f64,
|
||||
()
|
||||
);
|
||||
impl<T: SchemaType> SchemaType for Vec<T> {}
|
||||
impl<T: SchemaType> SchemaType for Option<T> {}
|
||||
|
||||
/// Generated main-interface dispatch.
|
||||
pub trait Interface: 'static {
|
||||
fn discover(&self) -> Value;
|
||||
fn dispatch<'a>(
|
||||
&'a self,
|
||||
method: &'a str,
|
||||
params: Value,
|
||||
ctx: &'a Ctx,
|
||||
) -> LocalBoxFuture<'a, Result<Value>>;
|
||||
fn stop(&self) -> LocalBoxFuture<'_, ()> {
|
||||
Box::pin(async {})
|
||||
}
|
||||
}
|
||||
|
||||
/// Context of an incoming call.
|
||||
#[derive(Default)]
|
||||
pub struct Ctx {
|
||||
exit: std::cell::Cell<bool>,
|
||||
}
|
||||
impl Ctx {
|
||||
pub fn notify(&self, name: &str, params: impl serde::Serialize) -> Result<()> {
|
||||
notify("core.notify", json!({"name":name,"params":params}))
|
||||
}
|
||||
pub fn exit_after_response(&self) {
|
||||
self.exit.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
type Notify = Box<dyn FnMut(&str, Value)>;
|
||||
struct Pending {
|
||||
tx: oneshot::Sender<Result<Value>>,
|
||||
notify: Option<Notify>,
|
||||
unlock: Option<std::rc::Rc<Lease>>,
|
||||
}
|
||||
struct Lease {
|
||||
path: String,
|
||||
held: std::cell::Cell<bool>,
|
||||
}
|
||||
impl Drop for Lease {
|
||||
fn drop(&mut self) {
|
||||
if self.held.get() {
|
||||
let _ = notify("core.unlock", json!({"path":self.path}));
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
active: bool,
|
||||
stopping: bool,
|
||||
next: u64,
|
||||
pending: HashMap<u64, Pending>,
|
||||
#[cfg(target_os = "wasi")]
|
||||
tasks: usize,
|
||||
#[cfg(target_os = "wasi")]
|
||||
wake: Option<std::task::Waker>,
|
||||
#[cfg(target_os = "wasi")]
|
||||
exit: bool,
|
||||
}
|
||||
thread_local! { static STATE: RefCell<State> = RefCell::new(State::default()); }
|
||||
fn send(value: Value) -> Result<()> {
|
||||
let bytes = format!("\n{value}\n").into_bytes();
|
||||
let written = std::io::stdout().write(&bytes)?;
|
||||
if written != bytes.len() {
|
||||
return Err(Error::new(-32603, "short protocol write"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn notify(method: &str, params: Value) -> Result<()> {
|
||||
if !STATE.with(|s| s.borrow().active) {
|
||||
return Err(Error::new(-32002, "no core transport"));
|
||||
}
|
||||
send(json!({"jsonrpc":"2.0","method":method,"params":params}))
|
||||
}
|
||||
|
||||
/// An outgoing request builder.
|
||||
pub struct Call {
|
||||
method: String,
|
||||
params: Value,
|
||||
detach: bool,
|
||||
notify: Option<Notify>,
|
||||
unlock: Option<std::rc::Rc<Lease>>,
|
||||
}
|
||||
pub fn call(method: impl Into<String>, params: impl serde::Serialize) -> Call {
|
||||
Call {
|
||||
method: method.into(),
|
||||
params: serde_json::to_value(params).expect("serializable params"),
|
||||
detach: false,
|
||||
notify: None,
|
||||
unlock: None,
|
||||
}
|
||||
}
|
||||
impl Call {
|
||||
pub fn detach(mut self) -> Self {
|
||||
self.detach = true;
|
||||
self
|
||||
}
|
||||
pub fn on_notify(mut self, f: impl FnMut(&str, Value) + 'static) -> Self {
|
||||
self.notify = Some(Box::new(f));
|
||||
self
|
||||
}
|
||||
}
|
||||
impl IntoFuture for Call {
|
||||
type Output = Result<Value>;
|
||||
type IntoFuture = LocalBoxFuture<'static, Self::Output>;
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
if !self.params.is_object() {
|
||||
return Err(Error::new(-32602, "params must be an object"));
|
||||
}
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let id = STATE.with(|s| {
|
||||
let mut s = s.borrow_mut();
|
||||
if !s.active || s.stopping {
|
||||
return Err(Error::new(
|
||||
-32002,
|
||||
"no core transport (or stdin reached EOF)",
|
||||
));
|
||||
}
|
||||
s.next += 1;
|
||||
let id = s.next;
|
||||
s.pending.insert(
|
||||
id,
|
||||
Pending {
|
||||
tx,
|
||||
notify: self.notify,
|
||||
unlock: self.unlock,
|
||||
},
|
||||
);
|
||||
Ok(id)
|
||||
})?;
|
||||
let (method, params) = if self.detach {
|
||||
(
|
||||
"core.call".to_string(),
|
||||
json!({"method":self.method,"params":self.params,"detach":true}),
|
||||
)
|
||||
} else {
|
||||
(self.method, self.params)
|
||||
};
|
||||
if let Err(e) = send(json!({"jsonrpc":"2.0","id":id,"method":method,"params":params})) {
|
||||
STATE.with(|s| s.borrow_mut().pending.remove(&id));
|
||||
return Err(e);
|
||||
}
|
||||
rx.await
|
||||
.unwrap_or_else(|_| Err(Error::new(-32002, "core transport closed")))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// An advisory lock acquisition.
|
||||
pub struct Lock {
|
||||
path: String,
|
||||
wait: bool,
|
||||
}
|
||||
pub fn lock(path: impl AsRef<Path>) -> Lock {
|
||||
Lock {
|
||||
path: path.as_ref().to_string_lossy().into_owned(),
|
||||
wait: true,
|
||||
}
|
||||
}
|
||||
impl Lock {
|
||||
pub fn try_now(mut self) -> Self {
|
||||
self.wait = false;
|
||||
self
|
||||
}
|
||||
}
|
||||
/// Releases its lock on drop, unless kept until process exit.
|
||||
pub struct Guard {
|
||||
path: Option<String>,
|
||||
}
|
||||
impl Guard {
|
||||
pub fn keep(mut self) {
|
||||
self.path.take();
|
||||
}
|
||||
pub fn held(&self) -> bool {
|
||||
self.path.is_some()
|
||||
}
|
||||
}
|
||||
impl Drop for Guard {
|
||||
fn drop(&mut self) {
|
||||
if let Some(path) = self.path.take() {
|
||||
let _ = notify("core.unlock", json!({"path":path}));
|
||||
}
|
||||
}
|
||||
}
|
||||
impl IntoFuture for Lock {
|
||||
type Output = Result<Guard>;
|
||||
type IntoFuture = LocalBoxFuture<'static, Self::Output>;
|
||||
fn into_future(self) -> Self::IntoFuture {
|
||||
Box::pin(async move {
|
||||
let lease = std::rc::Rc::new(Lease {
|
||||
path: self.path.clone(),
|
||||
held: std::cell::Cell::new(false),
|
||||
});
|
||||
let mut c = call("core.lock", json!({"path":self.path,"wait":self.wait}));
|
||||
c.unlock = Some(lease.clone());
|
||||
let result = c.await?;
|
||||
lease.held.set(false);
|
||||
Ok(Guard {
|
||||
path: if result["held"] == true {
|
||||
Some(self.path)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pid() -> String {
|
||||
std::env::var("SAUSAGE_PID").unwrap_or_default()
|
||||
}
|
||||
pub fn state_dir() -> PathBuf {
|
||||
PathBuf::from("/state").join(std::env::var("SAUSAGE_PLUGIN").unwrap_or_default())
|
||||
}
|
||||
pub fn write_atomic(path: impl AsRef<Path>, bytes: impl AsRef<[u8]>) -> Result<()> {
|
||||
let path = path.as_ref();
|
||||
let temp = path.with_extension(format!("tmp-{}", pid()));
|
||||
std::fs::write(&temp, bytes)?;
|
||||
std::fs::rename(temp, path)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "wasi")]
|
||||
pub fn block_on<F: Future>(f: F) -> F::Output {
|
||||
wstd::runtime::block_on(f)
|
||||
}
|
||||
#[cfg(not(target_os = "wasi"))]
|
||||
pub fn block_on<F: Future>(f: F) -> F::Output {
|
||||
futures::executor::block_on(f)
|
||||
}
|
||||
|
||||
/// Run an SDK test with a core transport when the WASI runner supplies one.
|
||||
#[cfg(target_os = "wasi")]
|
||||
pub fn test_run<F: Future>(f: F) -> F::Output {
|
||||
block_on(async {
|
||||
use wstd::io::AsyncRead;
|
||||
STATE.with(|s| s.borrow_mut().active = true);
|
||||
let reader = wstd::runtime::spawn(async {
|
||||
let mut input = wstd::io::stdin();
|
||||
let mut line = Vec::new();
|
||||
loop {
|
||||
let mut byte = [0];
|
||||
if !matches!(input.read(&mut byte).await, Ok(1)) {
|
||||
break;
|
||||
}
|
||||
if byte[0] == b'\n' {
|
||||
if let Ok(v) = serde_json::from_slice::<Value>(&line) {
|
||||
if v["jsonrpc"] == "2.0" {
|
||||
receive(v);
|
||||
}
|
||||
}
|
||||
line.clear();
|
||||
} else {
|
||||
line.push(byte[0]);
|
||||
}
|
||||
}
|
||||
STATE.with(|s| {
|
||||
let mut s = s.borrow_mut();
|
||||
s.stopping = true;
|
||||
s.pending.clear();
|
||||
});
|
||||
});
|
||||
let result = f.await;
|
||||
drop(reader);
|
||||
STATE.with(|s| {
|
||||
let mut s = s.borrow_mut();
|
||||
s.active = false;
|
||||
s.pending.clear();
|
||||
});
|
||||
result
|
||||
})
|
||||
}
|
||||
#[cfg(not(target_os = "wasi"))]
|
||||
pub fn test_run<F: Future>(f: F) -> F::Output {
|
||||
block_on(f)
|
||||
}
|
||||
|
||||
/// Register a task that keeps the process alive.
|
||||
#[cfg(target_os = "wasi")]
|
||||
pub fn background(f: impl Future<Output = ()> + 'static) {
|
||||
STATE.with(|s| s.borrow_mut().tasks += 1);
|
||||
wstd::runtime::spawn(async move {
|
||||
f.await;
|
||||
STATE.with(|s| {
|
||||
let mut s = s.borrow_mut();
|
||||
s.tasks -= 1;
|
||||
if let Some(w) = s.wake.take() {
|
||||
w.wake();
|
||||
}
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
#[cfg(not(target_os = "wasi"))]
|
||||
pub fn background(_f: impl Future<Output = ()> + 'static) {
|
||||
panic!("background requires the WASI runtime");
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn receive(value: Value) {
|
||||
if value["method"] == "core.notify" {
|
||||
if let Some(id) = value["params"]["call"].as_u64() {
|
||||
let callback = STATE.with(|s| {
|
||||
s.borrow_mut()
|
||||
.pending
|
||||
.get_mut(&id)
|
||||
.and_then(|p| p.notify.take())
|
||||
});
|
||||
if let Some(mut f) = callback {
|
||||
f(
|
||||
value["params"]["name"].as_str().unwrap_or_default(),
|
||||
value["params"]["params"].clone(),
|
||||
);
|
||||
STATE.with(|s| {
|
||||
if let Some(p) = s.borrow_mut().pending.get_mut(&id) {
|
||||
p.notify = Some(f);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
let pending = value["id"]
|
||||
.as_u64()
|
||||
.and_then(|id| STATE.with(|s| s.borrow_mut().pending.remove(&id)));
|
||||
if let Some(p) = pending {
|
||||
let result = if value.get("error").is_some() {
|
||||
serde_json::from_value(value["error"].clone()).map_or_else(|e| Err(e.into()), Err)
|
||||
} else {
|
||||
Ok(value["result"].clone())
|
||||
};
|
||||
if let Some(lease) = &p.unlock {
|
||||
lease
|
||||
.held
|
||||
.set(result.as_ref().is_ok_and(|v| v["held"] == true));
|
||||
}
|
||||
let _ = p.tx.send(result);
|
||||
} else {
|
||||
eprintln!("note: response with unknown ID");
|
||||
}
|
||||
}
|
||||
|
||||
/// Serve the main interface until its work ends or stdin reaches EOF.
|
||||
#[cfg(target_os = "wasi")]
|
||||
pub fn run<T: Interface>(value: T) {
|
||||
block_on(serve(value));
|
||||
}
|
||||
#[cfg(not(target_os = "wasi"))]
|
||||
pub fn run<T: Interface>(_value: T) {
|
||||
panic!("run requires WASI stdin/stdout");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "wasi")]
|
||||
async fn serve<T: Interface>(value: T) {
|
||||
use std::{rc::Rc, task::Poll};
|
||||
use wstd::io::AsyncRead;
|
||||
STATE.with(|s| {
|
||||
*s.borrow_mut() = State {
|
||||
active: true,
|
||||
..State::default()
|
||||
}
|
||||
});
|
||||
let value = Rc::new(value);
|
||||
let mut input = wstd::io::stdin();
|
||||
let mut line = Vec::new();
|
||||
let mut started = false;
|
||||
loop {
|
||||
let read = async {
|
||||
loop {
|
||||
let mut byte = [0];
|
||||
if input.read(&mut byte).await? == 0 {
|
||||
return Ok::<_, std::io::Error>(None);
|
||||
}
|
||||
if byte[0] == b'\n' {
|
||||
return Ok(Some(std::mem::take(&mut line)));
|
||||
}
|
||||
line.push(byte[0]);
|
||||
}
|
||||
};
|
||||
let idle = futures::future::poll_fn(|cx| {
|
||||
STATE.with(|s| {
|
||||
let mut s = s.borrow_mut();
|
||||
s.wake = Some(cx.waker().clone());
|
||||
if started && (s.tasks == 0 || s.exit) {
|
||||
Poll::Ready(())
|
||||
} else {
|
||||
Poll::Pending
|
||||
}
|
||||
})
|
||||
});
|
||||
futures::pin_mut!(read, idle);
|
||||
let bytes = match futures::future::select(read, idle).await {
|
||||
futures::future::Either::Left((Ok(Some(b)), _)) => b,
|
||||
futures::future::Either::Left((_, _)) => {
|
||||
STATE.with(|s| {
|
||||
let mut s = s.borrow_mut();
|
||||
s.stopping = true;
|
||||
s.pending.clear();
|
||||
});
|
||||
value.stop().await;
|
||||
break;
|
||||
}
|
||||
futures::future::Either::Right(_) => break,
|
||||
};
|
||||
let Ok(msg) = serde_json::from_slice::<Value>(&bytes) else {
|
||||
continue;
|
||||
};
|
||||
if msg["jsonrpc"] != "2.0" {
|
||||
continue;
|
||||
}
|
||||
if let Some(method) = msg["method"].as_str() {
|
||||
if method == "core.notify" {
|
||||
receive(msg);
|
||||
continue;
|
||||
}
|
||||
started = true;
|
||||
let value = value.clone();
|
||||
let method = method.to_owned();
|
||||
background(async move {
|
||||
let ctx = Ctx::default();
|
||||
let result = if method == "rpc.discover" {
|
||||
Ok(value.discover())
|
||||
} else {
|
||||
value
|
||||
.dispatch(
|
||||
&method,
|
||||
msg.get("params").cloned().unwrap_or(json!({})),
|
||||
&ctx,
|
||||
)
|
||||
.await
|
||||
};
|
||||
if let Some(id) = msg.get("id") {
|
||||
let response = match result {
|
||||
Ok(v) => json!({"jsonrpc":"2.0","id":id,"result":v}),
|
||||
Err(e) => json!({"jsonrpc":"2.0","id":id,"error":e}),
|
||||
};
|
||||
let _ = send(response);
|
||||
}
|
||||
if ctx.exit.get() {
|
||||
STATE.with(|s| s.borrow_mut().exit = true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
receive(msg);
|
||||
}
|
||||
}
|
||||
STATE.with(|s| {
|
||||
let mut s = s.borrow_mut();
|
||||
s.active = false;
|
||||
s.pending.clear();
|
||||
});
|
||||
}
|
||||
|
||||
/// Create a shared OpenRPC document.
|
||||
pub fn document(title: &str, version: &str) -> Value {
|
||||
sausage_interface::Document::new(title, version).into_value()
|
||||
}
|
||||
|
||||
/// Build OpenRPC entries with separate deserialize and serialize contracts.
|
||||
pub fn method<P: Params, R: SchemaType>(doc: &mut Value, name: &str, docs: &str) {
|
||||
fn generator(serialize: bool) -> schemars::SchemaGenerator {
|
||||
let mut s = schemars::generate::SchemaSettings::draft07();
|
||||
s.definitions_path = "/components/schemas".into();
|
||||
if serialize {
|
||||
s.for_serialize()
|
||||
} else {
|
||||
s.for_deserialize()
|
||||
}
|
||||
.into_generator()
|
||||
}
|
||||
let mut p = generator(false);
|
||||
let pref = serde_json::to_value(p.subschema_for::<P>()).unwrap();
|
||||
let mut defs = p.take_definitions(true);
|
||||
for schema in defs.values_mut() {
|
||||
normalize_enums(schema);
|
||||
}
|
||||
let ps = &defs[P::schema_name().as_ref()];
|
||||
let params:Vec<_>=ps["properties"].as_object().into_iter().flatten().map(|(name,s)| json!({"name":name,"description":s.get("description").cloned().unwrap_or(Value::Null),"required":ps["required"].as_array().is_some_and(|a|a.contains(&json!(name))),"schema":s})).collect();
|
||||
doc["components"]["schemas"]
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.extend(defs);
|
||||
let mut r = generator(true);
|
||||
let mut rs = serde_json::to_value(r.subschema_for::<R>()).unwrap();
|
||||
fn result_refs(value: &mut Value) {
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
if let Some(Value::String(reference)) = map.get_mut("$ref")
|
||||
&& reference.starts_with("#/components/schemas/")
|
||||
{
|
||||
reference.push_str("Result");
|
||||
}
|
||||
for child in map.values_mut() {
|
||||
result_refs(child);
|
||||
}
|
||||
}
|
||||
Value::Array(values) => {
|
||||
for child in values {
|
||||
result_refs(child);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
result_refs(&mut rs);
|
||||
for (name, mut schema) in r.take_definitions(true) {
|
||||
normalize_enums(&mut schema);
|
||||
result_refs(&mut schema);
|
||||
doc["components"]["schemas"]
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert(format!("{name}Result"), schema);
|
||||
}
|
||||
doc["methods"].as_array_mut().unwrap().push(json!({"name":name,"summary":docs.split("\n\n").next().unwrap_or_default(),"description":docs,"paramStructure":"by-name","params":params,"result":{"name":"result","schema":rs},"x-sausage-params":pref}));
|
||||
}
|
||||
// Schemars represents documented unit variants as oneOf/const. Export string enums instead.
|
||||
fn normalize_enums(value: &mut Value) {
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
if let Some(variants) = map.get("oneOf").and_then(Value::as_array) {
|
||||
let values: Option<Vec<Value>> = variants
|
||||
.iter()
|
||||
.map(|v| {
|
||||
if let Some(s) = v.get("const").filter(|v| v.is_string()) {
|
||||
Some(vec![s.clone()])
|
||||
} else {
|
||||
v.get("enum")
|
||||
.and_then(Value::as_array)
|
||||
.filter(|a| a.iter().all(Value::is_string))
|
||||
.cloned()
|
||||
}
|
||||
})
|
||||
.collect::<Option<Vec<Vec<Value>>>>()
|
||||
.map(|v| v.into_iter().flatten().collect());
|
||||
if let Some(values) = values {
|
||||
map.remove("oneOf");
|
||||
map.insert("type".into(), json!("string"));
|
||||
map.insert("enum".into(), json!(values));
|
||||
}
|
||||
}
|
||||
for child in map.values_mut() {
|
||||
normalize_enums(child);
|
||||
}
|
||||
}
|
||||
Value::Array(values) => {
|
||||
for child in values {
|
||||
normalize_enums(child);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Empty parameters for methods without arguments.
|
||||
#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct Empty {}
|
||||
impl SchemaType for Empty {}
|
||||
impl Params for Empty {}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
[package]
|
||||
name = "sdk-probe"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
[workspace]
|
||||
[dependencies]
|
||||
sausage-plugin = { path = "../../.." }
|
||||
@@ -0,0 +1,114 @@
|
||||
//! Probe of SDK transport and lifecycle behavior.
|
||||
use sausage_plugin::serde_json::{Value, json};
|
||||
use sausage_plugin::{Ctx, Result, call, lock, rpc, schema};
|
||||
struct Probe;
|
||||
/// The values to add.
|
||||
#[schema]
|
||||
struct Add {
|
||||
/// The first number.
|
||||
a: i32,
|
||||
/// The second number.
|
||||
b: i32,
|
||||
}
|
||||
/// The retired version.
|
||||
#[schema]
|
||||
struct Retired {
|
||||
/// The previous hash.
|
||||
previous: String,
|
||||
}
|
||||
#[rpc(grace_seconds = 12)]
|
||||
impl Probe {
|
||||
/// Add two numbers.
|
||||
///
|
||||
/// The result is their sum.
|
||||
fn add(&self, p: Add) -> Result<i32> {
|
||||
Ok(p.a + p.b)
|
||||
}
|
||||
/// Return a deliberate error.
|
||||
fn fail(&self) -> Result<String> {
|
||||
Err(sausage_plugin::Error::new(-32010, "deliberate"))
|
||||
}
|
||||
/// Exercise calls, notifications, and advisory locks.
|
||||
async fn exercise(&self, ctx: &Ctx) -> Result<String> {
|
||||
let a =
|
||||
call("other.first", json!({})).on_notify(|name, _| eprintln!("notification: {name}"));
|
||||
let b = call("other.second", json!({})).detach();
|
||||
let (a, b) = join(a, b).await;
|
||||
a?;
|
||||
b?;
|
||||
ctx.notify("progress", json!({"done":true}))?;
|
||||
{
|
||||
let guard = lock("/state/probe/file").await?;
|
||||
assert!(guard.held());
|
||||
}
|
||||
assert!(!lock("/state/probe/file").try_now().await?.held());
|
||||
Ok("done".into())
|
||||
}
|
||||
/// Drop an acquisition before the core grants its lock.
|
||||
async fn late(&self) -> Result<String> {
|
||||
{
|
||||
let mut future = std::pin::pin!(std::future::IntoFuture::into_future(lock("/late")));
|
||||
std::future::poll_fn(|cx| {
|
||||
assert!(future.as_mut().poll(cx).is_pending());
|
||||
std::task::Poll::Ready(())
|
||||
})
|
||||
.await;
|
||||
}
|
||||
call("other.barrier", json!({})).await?;
|
||||
lock("/kept").await?.keep();
|
||||
Ok("late done".into())
|
||||
}
|
||||
/// Mark a retired version.
|
||||
#[sausage_plugin::on_retired]
|
||||
fn retired(&self, p: Retired) -> Result<String> {
|
||||
Ok(p.previous)
|
||||
}
|
||||
/// Wait until EOF, then run the stop hook.
|
||||
async fn wait(&self) -> Result<String> {
|
||||
call("other.wait", json!({})).await?;
|
||||
Ok("done".into())
|
||||
}
|
||||
/// Keep the process alive with a background call.
|
||||
fn start(&self) -> Result<String> {
|
||||
sausage_plugin::background(async {
|
||||
let _ = call("other.background", json!({})).await;
|
||||
});
|
||||
Ok("started".into())
|
||||
}
|
||||
/// Save an EOF marker.
|
||||
#[sausage_plugin::on_stop]
|
||||
async fn stop(&self) {
|
||||
std::fs::write("/stopped", b"yes").unwrap();
|
||||
Ctx::default().notify("stopped", json!({})).unwrap();
|
||||
assert!(call("other.no", json!({})).await.is_err());
|
||||
}
|
||||
}
|
||||
async fn join(
|
||||
a: impl std::future::IntoFuture<Output = Result<Value>>,
|
||||
b: impl std::future::IntoFuture<Output = Result<Value>>,
|
||||
) -> (Result<Value>, Result<Value>) {
|
||||
let mut a = std::pin::pin!(a.into_future());
|
||||
let mut b = std::pin::pin!(b.into_future());
|
||||
let (mut av, mut bv) = (None, None);
|
||||
std::future::poll_fn(move |cx| {
|
||||
if av.is_none() {
|
||||
if let std::task::Poll::Ready(v) = a.as_mut().poll(cx) {
|
||||
av = Some(v);
|
||||
}
|
||||
}
|
||||
if bv.is_none() {
|
||||
if let std::task::Poll::Ready(v) = b.as_mut().poll(cx) {
|
||||
bv = Some(v);
|
||||
}
|
||||
}
|
||||
if av.is_some() && bv.is_some() {
|
||||
std::task::Poll::Ready((av.take().unwrap(), bv.take().unwrap()))
|
||||
} else {
|
||||
std::task::Poll::Pending
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
fn main() {
|
||||
sausage_plugin::run(Probe)
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
//! Native checks for generated schemas, dispatch, and missing core transports.
|
||||
use sausage_plugin::serde_json::json;
|
||||
use sausage_plugin::{Ctx, Interface, Result, rpc, schema};
|
||||
/// The addition parameters.
|
||||
#[schema]
|
||||
struct Add {
|
||||
/// The first integer.
|
||||
a: i32,
|
||||
/// The second integer.
|
||||
b: i32,
|
||||
}
|
||||
/// A type with different serialize and deserialize contracts.
|
||||
#[schema]
|
||||
struct Defaulted {
|
||||
/// A value which the provider defaults.
|
||||
#[serde(default)]
|
||||
value: String,
|
||||
}
|
||||
/// A documented string enum.
|
||||
#[schema]
|
||||
enum Choice {
|
||||
/// The first choice.
|
||||
First,
|
||||
/// The second choice.
|
||||
Second,
|
||||
}
|
||||
struct Plugin;
|
||||
#[rpc]
|
||||
impl Plugin {
|
||||
/// Choose the first value.
|
||||
fn choice(&self) -> Result<Choice> {
|
||||
Ok(Choice::First)
|
||||
}
|
||||
/// Round-trip the defaulted object.
|
||||
fn defaults(&self, p: Defaulted) -> Result<Defaulted> {
|
||||
Ok(p)
|
||||
}
|
||||
/// Add integers.
|
||||
///
|
||||
/// Return their sum.
|
||||
fn add(&self, p: Add) -> Result<i32> {
|
||||
Ok(p.a + p.b)
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn discovery_and_dispatch() {
|
||||
let doc = Plugin.discover();
|
||||
assert!(
|
||||
doc["components"]["schemas"]["Defaulted"]
|
||||
.get("required")
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(
|
||||
doc["components"]["schemas"]["DefaultedResult"]["required"],
|
||||
json!(["value"])
|
||||
);
|
||||
assert_eq!(
|
||||
doc["components"]["schemas"]["ChoiceResult"]["enum"],
|
||||
json!(["First", "Second"])
|
||||
);
|
||||
let add = doc["methods"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|m| m["name"] == "add")
|
||||
.unwrap();
|
||||
assert_eq!(add["summary"], "Add integers.");
|
||||
assert_eq!(
|
||||
doc["components"]["schemas"]["Add"]["description"],
|
||||
"The addition parameters."
|
||||
);
|
||||
assert_eq!(add["params"][0]["description"], "The first integer.");
|
||||
eprintln!("DISCOVER={doc}");
|
||||
assert_eq!(
|
||||
sausage_plugin::block_on(Plugin.dispatch("add", json!({"a":2,"b":3}), &Ctx::default()))
|
||||
.unwrap(),
|
||||
5
|
||||
);
|
||||
assert!(sausage_plugin::block_on(Plugin.dispatch("add", json!({}), &Ctx::default())).is_err());
|
||||
}
|
||||
#[sausage_plugin::test]
|
||||
async fn missing_core_fails_without_wait() {
|
||||
assert!(
|
||||
Ctx::default()
|
||||
.notify("progress", json!({}))
|
||||
.unwrap_err()
|
||||
.message
|
||||
.contains("no core transport")
|
||||
);
|
||||
assert!(
|
||||
sausage_plugin::call("core.lock", json!({}))
|
||||
.await
|
||||
.unwrap_err()
|
||||
.message
|
||||
.contains("no core transport")
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn compile_fail() {
|
||||
trybuild::TestCases::new().compile_fail("tests/ui/*.rs");
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
//! Reject data-carrying enums.
|
||||
/// An invalid enum.
|
||||
#[sausage_plugin::schema]
|
||||
enum Bad {
|
||||
/// A payload variant.
|
||||
Value(String),
|
||||
}
|
||||
fn main() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: schema enums require unit variants
|
||||
--> tests/ui/enum.rs:5:5
|
||||
|
|
||||
5 | /// A payload variant.
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Require field documentation.
|
||||
/// A documented type.
|
||||
#[sausage_plugin::schema]
|
||||
struct Bad { value: String }
|
||||
fn main() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: a doc comment is required
|
||||
--> tests/ui/field_docs.rs:4:14
|
||||
|
|
||||
4 | struct Bad { value: String }
|
||||
| ^^^^^
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Require method documentation.
|
||||
struct Bad;
|
||||
#[sausage_plugin::rpc]
|
||||
impl Bad { fn bad(&self)->sausage_plugin::Result<()> {Ok(())} }
|
||||
fn main() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: a doc comment is required
|
||||
--> tests/ui/method_docs.rs:4:12
|
||||
|
|
||||
4 | impl Bad { fn bad(&self)->sausage_plugin::Result<()> {Ok(())} }
|
||||
| ^^
|
||||
@@ -0,0 +1,4 @@
|
||||
//! Reject undocumented schemas.
|
||||
#[sausage_plugin::schema]
|
||||
struct Bad { value: String }
|
||||
fn main() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: a doc comment is required
|
||||
--> tests/ui/missing_docs.rs:3:8
|
||||
|
|
||||
3 | struct Bad { value: String }
|
||||
| ^^^
|
||||
@@ -0,0 +1,8 @@
|
||||
//! Reject primitive params.
|
||||
struct Bad;
|
||||
#[sausage_plugin::rpc]
|
||||
impl Bad {
|
||||
/// Invalid params.
|
||||
fn bad(&self, value:String)->sausage_plugin::Result<String> {Ok(value)}
|
||||
}
|
||||
fn main() {}
|
||||
@@ -0,0 +1,34 @@
|
||||
error[E0277]: RPC params must be a #[schema] struct with named fields
|
||||
--> tests/ui/params.rs:6:25
|
||||
|
|
||||
6 | fn bad(&self, value:String)->sausage_plugin::Result<String> {Ok(value)}
|
||||
| ^^^^^^ the trait `Params` is not implemented for `std::string::String`
|
||||
|
|
||||
help: the trait `Params` is implemented for `sausage_plugin::Empty`
|
||||
--> src/lib.rs
|
||||
|
|
||||
| impl Params for Empty {}
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
note: required by a bound in `method`
|
||||
--> src/lib.rs
|
||||
|
|
||||
| pub fn method<P: Params, R: SchemaType>(doc: &mut Value, name: &str, docs: &str) {
|
||||
| ^^^^^^ required by this bound in `method`
|
||||
|
||||
error[E0277]: RPC params must be a #[schema] struct with named fields
|
||||
--> tests/ui/params.rs:6:25
|
||||
|
|
||||
6 | fn bad(&self, value:String)->sausage_plugin::Result<String> {Ok(value)}
|
||||
| ^^^^^^ the trait `Params` is not implemented for `std::string::String`
|
||||
|
|
||||
help: the trait `Params` is implemented for `sausage_plugin::Empty`
|
||||
--> src/lib.rs
|
||||
|
|
||||
| impl Params for Empty {}
|
||||
| ^^^^^^^^^^^^^^^^^^^^^
|
||||
note: required by a bound in `params_type`
|
||||
--> tests/ui/params.rs:3:1
|
||||
|
|
||||
3 | #[sausage_plugin::rpc]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `params_type`
|
||||
= note: this error originates in the attribute macro `sausage_plugin::rpc` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
@@ -0,0 +1,9 @@
|
||||
//! Reject serde shapes outside the subset.
|
||||
/// A schema with flatten.
|
||||
#[sausage_plugin::schema]
|
||||
struct Bad {
|
||||
/// A flattened value.
|
||||
#[serde(flatten)]
|
||||
value: String,
|
||||
}
|
||||
fn main() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: serde attribute is outside the schema subset
|
||||
--> tests/ui/serde.rs:6:13
|
||||
|
|
||||
6 | #[serde(flatten)]
|
||||
| ^^^^^^^
|
||||
@@ -0,0 +1,8 @@
|
||||
//! Reject invalid receiver signatures.
|
||||
struct Bad;
|
||||
#[sausage_plugin::rpc]
|
||||
impl Bad {
|
||||
/// An invalid receiver.
|
||||
fn bad(&mut self) -> sausage_plugin::Result<()> {Ok(())}
|
||||
}
|
||||
fn main() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: RPC methods require &self
|
||||
--> tests/ui/signature.rs:6:5
|
||||
|
|
||||
6 | fn bad(&mut self) -> sausage_plugin::Result<()> {Ok(())}
|
||||
| ^^
|
||||
@@ -0,0 +1,8 @@
|
||||
//! Reject unsupported field types.
|
||||
/// An invalid schema.
|
||||
#[sausage_plugin::schema]
|
||||
struct Bad {
|
||||
/// An unsupported dictionary.
|
||||
value: std::collections::HashMap<String,String>,
|
||||
}
|
||||
fn main() {}
|
||||
@@ -0,0 +1,22 @@
|
||||
error[E0277]: this type is outside the Sausage schema subset
|
||||
--> tests/ui/subset.rs:6:12
|
||||
|
|
||||
6 | value: std::collections::HashMap<String,String>,
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `SchemaType` is not implemented for `HashMap<std::string::String, std::string::String>`
|
||||
|
|
||||
= help: the following other types implement trait `SchemaType`:
|
||||
()
|
||||
Bad
|
||||
Vec<T>
|
||||
bool
|
||||
f32
|
||||
f64
|
||||
i16
|
||||
i32
|
||||
and $N others
|
||||
note: required by a bound in `supported`
|
||||
--> tests/ui/subset.rs:3:1
|
||||
|
|
||||
3 | #[sausage_plugin::schema]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `supported`
|
||||
= note: this error originates in the attribute macro `sausage_plugin::schema` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Reject tuple params.
|
||||
/// A tuple is not a named object.
|
||||
#[sausage_plugin::schema]
|
||||
struct Bad(String);
|
||||
fn main() {}
|
||||
@@ -0,0 +1,5 @@
|
||||
error: schema structs require named fields
|
||||
--> tests/ui/tuple.rs:2:1
|
||||
|
|
||||
2 | /// A tuple is not a named object.
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
@@ -0,0 +1,147 @@
|
||||
//! End-to-end protocol checks against a WASI plugin under Wasmtime.
|
||||
use sausage_plugin::serde_json::{Value, json};
|
||||
use std::{
|
||||
io::{BufRead, BufReader, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
struct Process {
|
||||
child: std::process::Child,
|
||||
input: Option<std::process::ChildStdin>,
|
||||
output: BufReader<std::process::ChildStdout>,
|
||||
}
|
||||
impl Process {
|
||||
fn send(&mut self, v: Value) {
|
||||
writeln!(self.input.as_mut().unwrap(), "{v}").unwrap();
|
||||
}
|
||||
fn read(&mut self) -> Value {
|
||||
loop {
|
||||
let mut s = String::new();
|
||||
assert_ne!(self.output.read_line(&mut s).unwrap(), 0, "unexpected EOF");
|
||||
if let Ok(v) = sausage_plugin::serde_json::from_str::<Value>(&s)
|
||||
&& v["jsonrpc"] == "2.0"
|
||||
{
|
||||
return v;
|
||||
}
|
||||
}
|
||||
}
|
||||
fn request(&mut self, method: &str, params: Value) {
|
||||
self.send(json!({"jsonrpc":"2.0","id":"probe-1","method":method,"params":params}));
|
||||
}
|
||||
fn answer(&mut self, r: &Value, result: Value) {
|
||||
self.send(json!({"jsonrpc":"2.0","id":r["id"],"result":result}));
|
||||
}
|
||||
}
|
||||
fn start(wasm: &Path, dir: &Path) -> Process {
|
||||
let mut child = Command::new("wasmtime")
|
||||
.args(["run", "--dir"])
|
||||
.arg(format!("{}::/", dir.display()))
|
||||
.arg(wasm)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap();
|
||||
let input = child.stdin.take();
|
||||
let output = BufReader::new(child.stdout.take().unwrap());
|
||||
Process {
|
||||
child,
|
||||
input,
|
||||
output,
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn protocol() {
|
||||
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let target = root.join("../target/sdk/probe");
|
||||
assert!(
|
||||
Command::new("cargo")
|
||||
.env("CARGO_TARGET_DIR", &target)
|
||||
.args(["build", "--offline", "--manifest-path"])
|
||||
.arg(root.join("tests/plugins/probe/Cargo.toml"))
|
||||
.args(["--target", "wasm32-wasip2"])
|
||||
.status()
|
||||
.unwrap()
|
||||
.success()
|
||||
);
|
||||
let wasm = target.join("wasm32-wasip2/debug/sdk-probe.wasm");
|
||||
let dir = std::env::temp_dir().join(format!("sausage-sdk-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let mut p = start(&wasm, &dir);
|
||||
p.request("rpc.discover", json!({}));
|
||||
let d = p.read();
|
||||
assert_eq!(d["result"]["x-sausage-grace"], 12);
|
||||
assert_eq!(
|
||||
d["result"]["methods"][0]["description"],
|
||||
"Add two numbers.\n\nThe result is their sum."
|
||||
);
|
||||
assert!(p.child.wait().unwrap().success());
|
||||
let mut p = start(&wasm, &dir);
|
||||
p.request("add", json!({"a":2,"b":4}));
|
||||
assert_eq!(p.read()["result"], 6);
|
||||
assert!(p.child.wait().unwrap().success());
|
||||
let mut p = start(&wasm, &dir);
|
||||
p.request("fail", json!({}));
|
||||
assert_eq!(p.read()["error"]["code"], -32010);
|
||||
assert!(p.child.wait().unwrap().success());
|
||||
let mut p = start(&wasm, &dir);
|
||||
p.request("exercise", json!({}));
|
||||
let a = p.read();
|
||||
let b = p.read();
|
||||
assert_eq!(a["method"], "other.first");
|
||||
assert_eq!(b["method"], "core.call");
|
||||
assert_eq!(b["params"]["detach"], true);
|
||||
p.send(json!({"jsonrpc":"2.0","id":9876,"result":{}}));
|
||||
p.send(json!({"jsonrpc":"2.0","method":"core.notify","params":{"call":a["id"],"name":"tick","params":{}}}));
|
||||
p.answer(&b, json!({}));
|
||||
p.answer(&a, json!({}));
|
||||
assert_eq!(p.read()["method"], "core.notify");
|
||||
let grant = p.read();
|
||||
assert_eq!(grant["method"], "core.lock");
|
||||
p.answer(&grant, json!({"held":true}));
|
||||
assert_eq!(p.read()["method"], "core.unlock");
|
||||
let attempt = p.read();
|
||||
assert_eq!(attempt["params"]["wait"], false);
|
||||
p.answer(&attempt, json!({"held":false}));
|
||||
assert_eq!(p.read()["result"], "done");
|
||||
assert!(p.child.wait().unwrap().success());
|
||||
let mut err = String::new();
|
||||
std::io::Read::read_to_string(p.child.stderr.as_mut().unwrap(), &mut err).unwrap();
|
||||
assert!(err.contains("unknown ID"));
|
||||
assert!(err.contains("notification: tick"));
|
||||
let mut p = start(&wasm, &dir);
|
||||
p.request("rpc.retired", json!({"previous":"hash"}));
|
||||
assert_eq!(p.read()["result"], "hash");
|
||||
assert!(p.child.wait().unwrap().success());
|
||||
let mut p = start(&wasm, &dir);
|
||||
p.request("late", json!({}));
|
||||
let grant = p.read();
|
||||
let barrier = p.read();
|
||||
p.answer(&grant, json!({"held":true}));
|
||||
assert_eq!(p.read()["method"], "core.unlock");
|
||||
p.answer(&barrier, json!({}));
|
||||
let kept = p.read();
|
||||
p.answer(&kept, json!({"held":true}));
|
||||
assert_eq!(p.read()["result"], "late done");
|
||||
assert!(p.child.wait().unwrap().success());
|
||||
let mut p = start(&wasm, &dir);
|
||||
p.request("wait", json!({}));
|
||||
assert_eq!(p.read()["method"], "other.wait");
|
||||
drop(p.input.take());
|
||||
assert_eq!(p.read()["params"]["name"], "stopped");
|
||||
assert!(p.child.wait().unwrap().success());
|
||||
assert_eq!(std::fs::read(dir.join("stopped")).unwrap(), b"yes");
|
||||
let mut p = start(&wasm, &dir);
|
||||
p.request("start", json!({}));
|
||||
let first = p.read();
|
||||
let second = p.read();
|
||||
let outgoing = if first["method"] == "other.background" {
|
||||
first
|
||||
} else {
|
||||
second
|
||||
};
|
||||
assert_eq!(outgoing["method"], "other.background");
|
||||
p.answer(&outgoing, json!({}));
|
||||
assert!(p.child.wait().unwrap().success());
|
||||
std::fs::remove_dir_all(dir).unwrap();
|
||||
}
|
||||
Reference in New Issue
Block a user