sdk: readable clients (no Empty wrapper, no unused error module), documented examples with tests
This commit is contained in:
Generated
+99
@@ -724,6 +724,15 @@ 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"
|
||||
@@ -1156,6 +1165,26 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cc00ea907cab49550b7da656f80ebb97be1b997d931fbcd28d39734e17ce592"
|
||||
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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.13"
|
||||
@@ -1264,6 +1293,26 @@ 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"
|
||||
@@ -1369,6 +1418,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1788edb87fdc09c7e26304471e2f5be8cdefb1b6930d6e3985fc02ff53bf86ee"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
@@ -1379,6 +1429,33 @@ 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"
|
||||
@@ -1826,6 +1903,15 @@ 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"
|
||||
@@ -1836,7 +1922,10 @@ dependencies = [
|
||||
"http",
|
||||
"http-body-util",
|
||||
"include_dir",
|
||||
"notify",
|
||||
"rusqlite",
|
||||
"sausage-interface",
|
||||
"sausage-vfs",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
@@ -2558,6 +2647,16 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "want"
|
||||
version = "0.3.2"
|
||||
|
||||
@@ -28,10 +28,6 @@
|
||||
"count"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Empty": {
|
||||
"description": "Empty parameters for methods without arguments.",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -80,10 +76,7 @@
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"summary": "List all notes, oldest first.",
|
||||
"x-sausage-params": {
|
||||
"$ref": "#/components/schemas/Empty"
|
||||
}
|
||||
"summary": "List all notes, oldest first."
|
||||
}
|
||||
],
|
||||
"openrpc": "1.2.6",
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
{
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Empty": {
|
||||
"description": "Empty parameters for methods without arguments.",
|
||||
"type": "object"
|
||||
},
|
||||
"GetParams": {
|
||||
"description": "The name of a secret to read.",
|
||||
"properties": {
|
||||
@@ -54,10 +50,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"summary": "Open the in-memory vault and publish its endpoint.",
|
||||
"x-sausage-params": {
|
||||
"$ref": "#/components/schemas/Empty"
|
||||
}
|
||||
"summary": "Open the in-memory vault and publish its endpoint."
|
||||
}
|
||||
],
|
||||
"openrpc": "1.2.6",
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
{
|
||||
"components": {
|
||||
"schemas": {
|
||||
"Empty": {
|
||||
"description": "Empty parameters for methods without arguments.",
|
||||
"type": "object"
|
||||
},
|
||||
"StartParams": {
|
||||
"description": "The server configuration.",
|
||||
"properties": {
|
||||
@@ -75,10 +71,7 @@
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"summary": "Count the HTTP requests this server answered.",
|
||||
"x-sausage-params": {
|
||||
"$ref": "#/components/schemas/Empty"
|
||||
}
|
||||
"summary": "Count the HTTP requests this server answered."
|
||||
},
|
||||
{
|
||||
"description": "End this process after the response reaches the client.",
|
||||
@@ -91,10 +84,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"summary": "End this process after the response reaches the client.",
|
||||
"x-sausage-params": {
|
||||
"$ref": "#/components/schemas/Empty"
|
||||
}
|
||||
"summary": "End this process after the response reaches the client."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
//! Use typed calls from discovered provider documents in `imports/`.
|
||||
//! Generated modules own the wire types, so calls need no manual JSON.
|
||||
//! The detached HTTP service survives this consumer. Its endpoint refers to that service's process-local state.
|
||||
//! Copy fresh documents from `/proc/core/plugins/<provider>/discover.json` after an interface changes.
|
||||
use sausage_plugin::{Result, rpc, schema};
|
||||
sausage_plugin::import!("notes");
|
||||
sausage_plugin::import!("www");
|
||||
sausage_plugin::import!("secrets");
|
||||
/// Demonstrate typed imports without manual JSON messages.
|
||||
struct Consumer;
|
||||
/// A note for the imported notes plugin.
|
||||
#[schema]
|
||||
struct NoteParams {
|
||||
/// The text to append.
|
||||
text: String,
|
||||
}
|
||||
/// The text and HTTP port for this demonstration.
|
||||
#[schema]
|
||||
struct DemoParams {
|
||||
@@ -15,6 +24,10 @@ struct DemoParams {
|
||||
}
|
||||
#[rpc]
|
||||
impl Consumer {
|
||||
/// Add a note through the generated import and return its count.
|
||||
async fn note(&self, p: NoteParams) -> Result<usize> {
|
||||
Ok(notes::add(notes::AddParams { text: p.text }).await?.count as usize)
|
||||
}
|
||||
/// Add a note, start a server, and inspect its control endpoint.
|
||||
async fn demo(&self, p: DemoParams) -> Result<u64> {
|
||||
let added = notes::add(notes::AddParams { text: p.text }).await?;
|
||||
@@ -42,6 +55,34 @@ impl Consumer {
|
||||
.await
|
||||
}
|
||||
}
|
||||
/// Start the SDK transport for this plugin.
|
||||
fn main() {
|
||||
sausage_plugin::run(Consumer)
|
||||
}
|
||||
/// Tests demonstrate generated params and calls through the test core.
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
/// Generated params retain their wire field names.
|
||||
#[test]
|
||||
fn generated_params_use_text() {
|
||||
let value = sausage_plugin::serde_json::to_value(notes::AddParams {
|
||||
text: "milk".into(),
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(value, sausage_plugin::serde_json::json!({"text":"milk"}));
|
||||
}
|
||||
/// A typed import reaches the notes plugin through the test core.
|
||||
#[sausage_plugin::test]
|
||||
async fn imports_notes() {
|
||||
assert_eq!(
|
||||
Consumer
|
||||
.note(NoteParams {
|
||||
text: "milk".into()
|
||||
})
|
||||
.await
|
||||
.unwrap(),
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
//! Persistent notes. Each request reads the shared file instead of process memory.
|
||||
//! Persistent notes in `/state/notes/notes.json`.
|
||||
//! Each call has its own process. The add method locks the file before its read-modify-write operation.
|
||||
//! Atomic replacement lets readers observe a complete list without a lock.
|
||||
use sausage_plugin::serde_json;
|
||||
use sausage_plugin::{Result, lock, rpc, schema, state_dir, write_atomic};
|
||||
/// The stateless notes plugin reads its file on each call.
|
||||
struct Notes;
|
||||
/// Parameters for a new note.
|
||||
#[schema]
|
||||
@@ -31,6 +34,7 @@ impl Notes {
|
||||
load()
|
||||
}
|
||||
}
|
||||
/// Read the list, or return an empty list when the file does not exist.
|
||||
fn load() -> Result<Vec<String>> {
|
||||
match std::fs::read(state_dir().join("notes.json")) {
|
||||
Ok(bytes) => Ok(serde_json::from_slice(&bytes)?),
|
||||
@@ -38,6 +42,43 @@ fn load() -> Result<Vec<String>> {
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
/// Start the SDK transport for notes.
|
||||
fn main() {
|
||||
sausage_plugin::run(Notes)
|
||||
}
|
||||
|
||||
/// Tests run in fresh worlds under the WASI test runner.
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
/// The core lock protects two sequential additions and their counts.
|
||||
#[sausage_plugin::test]
|
||||
async fn add_appends_and_counts() {
|
||||
assert_eq!(
|
||||
Notes
|
||||
.add(AddParams {
|
||||
text: "milk".into()
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.count,
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
Notes
|
||||
.add(AddParams {
|
||||
text: "eggs".into()
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.count,
|
||||
2
|
||||
);
|
||||
assert_eq!(Notes.list().unwrap(), ["milk", "eggs"]);
|
||||
}
|
||||
/// A fresh test world has no notes file.
|
||||
#[test]
|
||||
fn list_is_empty_without_a_file() {
|
||||
assert!(Notes.list().unwrap().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//! Start `secrets.serve` as a detached boot service. Its memory lasts until process exit.
|
||||
use sausage_plugin::{Ctx, Result, listen, rpc, schema};
|
||||
use std::{cell::RefCell, collections::BTreeMap, rc::Rc};
|
||||
/// The service entry point publishes one process-local vault.
|
||||
struct Secrets;
|
||||
#[rpc(endpoints(vault=Vault))]
|
||||
impl Secrets {
|
||||
@@ -10,8 +11,10 @@ impl Secrets {
|
||||
listen("/run/secrets", Rc::new(Vault::default())).await
|
||||
}
|
||||
}
|
||||
/// The endpoint owns secrets in memory, not on disk.
|
||||
#[derive(Default)]
|
||||
struct Vault {
|
||||
/// Named values disappear when this process ends.
|
||||
entries: RefCell<BTreeMap<String, String>>,
|
||||
}
|
||||
/// The name and value of a secret.
|
||||
@@ -41,6 +44,47 @@ impl Vault {
|
||||
Ok(self.entries.borrow().get(&p.name).cloned())
|
||||
}
|
||||
}
|
||||
/// Start the SDK transport for the vault service.
|
||||
fn main() {
|
||||
sausage_plugin::run(Secrets)
|
||||
}
|
||||
/// Tests verify absent keys and process-local storage.
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
/// Missing names return null instead of an RPC error.
|
||||
#[test]
|
||||
fn absent_secret() {
|
||||
assert_eq!(
|
||||
Vault::default()
|
||||
.get(GetParams {
|
||||
name: "missing".into()
|
||||
})
|
||||
.unwrap(),
|
||||
None
|
||||
);
|
||||
}
|
||||
/// Store a value and retrieve it through the same process-local vault.
|
||||
#[sausage_plugin::test]
|
||||
async fn stores_secret() {
|
||||
let vault = Vault::default();
|
||||
vault
|
||||
.put(
|
||||
&Ctx::default(),
|
||||
PutParams {
|
||||
name: "token".into(),
|
||||
value: "example".into(),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
vault
|
||||
.get(GetParams {
|
||||
name: "token".into()
|
||||
})
|
||||
.unwrap()
|
||||
.as_deref(),
|
||||
Some("example")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
//! Serve files from `/state/www` and expose a control endpoint at `/self/control`.
|
||||
//! Start this process detached or as a service so it outlives its caller.
|
||||
//! Clients read `/proc/<pid>/control`. A fresh core call cannot inspect this process's counters.
|
||||
use sausage_plugin::{Ctx, Result, background, listen, pid, rpc, schema, state_dir};
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
use wstd::{
|
||||
@@ -6,6 +8,7 @@ use wstd::{
|
||||
iter::AsyncIterator,
|
||||
net::TcpListener,
|
||||
};
|
||||
/// The HTTP plugin starts a server and returns its process ID.
|
||||
struct Www;
|
||||
/// The server configuration.
|
||||
#[schema]
|
||||
@@ -24,7 +27,9 @@ impl Www {
|
||||
Ok(pid())
|
||||
}
|
||||
}
|
||||
/// Shared process-local state for the HTTP task and its endpoint.
|
||||
struct Control {
|
||||
/// The number of answered requests.
|
||||
hits: Cell<u64>,
|
||||
}
|
||||
#[rpc]
|
||||
@@ -39,6 +44,7 @@ impl Control {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
/// Answer HTTP requests with files and count all responses, including missing files.
|
||||
async fn serve(tcp: TcpListener, control: Rc<Control>) {
|
||||
let mut incoming = tcp.incoming();
|
||||
while let Some(Ok(mut stream)) = incoming.next().await {
|
||||
@@ -70,6 +76,22 @@ async fn serve(tcp: TcpListener, control: Rc<Control>) {
|
||||
let _ = stream.flush().await;
|
||||
}
|
||||
}
|
||||
/// Start the SDK transport for the HTTP plugin.
|
||||
fn main() {
|
||||
sausage_plugin::run(Www)
|
||||
}
|
||||
/// Tests check local counters and the service entry point.
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
/// Control reports its shared counter without a core round trip.
|
||||
#[test]
|
||||
fn stats_reads_counter() {
|
||||
assert_eq!(Control { hits: Cell::new(3) }.stats().unwrap(), 3);
|
||||
}
|
||||
/// The listener binds an ephemeral port and publishes its control endpoint.
|
||||
#[sausage_plugin::test]
|
||||
async fn starts_on_available_port() {
|
||||
assert_eq!(Www.start(StartParams { port: 0 }).await.unwrap(), pid());
|
||||
}
|
||||
}
|
||||
|
||||
+31
-5
@@ -56,20 +56,33 @@ pub fn client_rs(name: &str, doc: &Value) -> Result<String, String> {
|
||||
local_refs(&mut ps);
|
||||
let mut rs = method["result"]["schema"].clone();
|
||||
local_refs(&mut rs);
|
||||
let p = space
|
||||
.add_type(&serde_json::from_value(ps).map_err(|e| e.to_string())?)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let empty = method["params"].as_array().is_none_or(Vec::is_empty);
|
||||
let p = if empty {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
space
|
||||
.add_type(&serde_json::from_value(ps).map_err(|e| e.to_string())?)
|
||||
.map_err(|e| e.to_string())?,
|
||||
)
|
||||
};
|
||||
let r = space
|
||||
.add_type(&serde_json::from_value(rs).map_err(|e| e.to_string())?)
|
||||
.map_err(|e| e.to_string())?;
|
||||
let p = space.get_type(&p).map_err(|e| e.to_string())?.ident();
|
||||
let p = p
|
||||
.map(|p| {
|
||||
space
|
||||
.get_type(&p)
|
||||
.map(|p| p.ident())
|
||||
.map_err(|e| e.to_string())
|
||||
})
|
||||
.transpose()?;
|
||||
let r = space.get_type(&r).map_err(|e| e.to_string())?.ident();
|
||||
let docs = method
|
||||
.get("description")
|
||||
.or_else(|| method.get("summary"))
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or("");
|
||||
let empty = method["params"].as_array().is_none_or(Vec::is_empty);
|
||||
let arg = if empty { quote!() } else { quote!(params:#p) };
|
||||
let params = if empty {
|
||||
quote!(::sausage_plugin::serde_json::json!({}))
|
||||
@@ -122,6 +135,19 @@ pub fn client_rs(name: &str, doc: &Value) -> Result<String, String> {
|
||||
}
|
||||
serde_attrs(std::slice::from_mut(item));
|
||||
}
|
||||
let uses_error = file
|
||||
.items
|
||||
.iter()
|
||||
.filter(|item| !matches!(item, syn::Item::Mod(m) if m.ident == "error"))
|
||||
.any(|item| {
|
||||
quote!(#item)
|
||||
.to_string()
|
||||
.contains("error :: ConversionError")
|
||||
});
|
||||
if !uses_error {
|
||||
file.items
|
||||
.retain(|item| !matches!(item, syn::Item::Mod(m) if m.ident == "error"));
|
||||
}
|
||||
let types = &file.items;
|
||||
let mut file =
|
||||
syn::parse2::<syn::File>(quote!(mod #module {#(#types)* #(#functions)* #(#endpoints)*}))
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
//! Check the shared generator against a real example's discovered document.
|
||||
|
||||
/// String enums retain the conversion support that their parsers need.
|
||||
#[test]
|
||||
fn enum_client_keeps_used_error_support() {
|
||||
let doc = serde_json::json!({
|
||||
"components":{"schemas":{"Mode":{"type":"string","enum":["fast","slow"]}}},
|
||||
"methods":[{"name":"mode","params":[],"result":{"schema":{"$ref":"#/components/schemas/Mode"}}}]
|
||||
});
|
||||
let code = sausage_interface::client_rs("settings", &doc).unwrap();
|
||||
assert!(code.contains("pub mod error"));
|
||||
assert!(code.contains("error::ConversionError"));
|
||||
assert!(!code.contains("ModeParams"));
|
||||
syn::parse_file(&code).unwrap();
|
||||
}
|
||||
#[test]
|
||||
fn notes_client_preserves_names_and_docs() {
|
||||
let document =
|
||||
@@ -7,6 +21,8 @@ fn notes_client_preserves_names_and_docs() {
|
||||
assert!(code.contains("pub struct AddParams"));
|
||||
assert!(code.contains("pub struct Added"));
|
||||
assert!(!code.contains("AddedResult"));
|
||||
assert!(!code.contains("pub struct Empty"));
|
||||
assert!(!code.contains("pub mod error"));
|
||||
assert!(code.contains("The complete note text"));
|
||||
assert!(code.contains("pub fn add(params: AddParams)"));
|
||||
syn::parse_file(&code).unwrap();
|
||||
|
||||
+13
-7
@@ -68,25 +68,31 @@ pub fn method<P: Params, R: SchemaType>(doc: &mut Value, name: &str, docs: &str)
|
||||
"schema": field,
|
||||
})
|
||||
}).collect();
|
||||
doc["x-sdk-input"]
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.extend(definitions);
|
||||
if !params.is_empty() {
|
||||
doc["x-sdk-input"]
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.extend(definitions);
|
||||
}
|
||||
let mut output = generator(true);
|
||||
let result_schema = serde_json::to_value(output.subschema_for::<R>()).unwrap();
|
||||
doc["x-sdk-output"]
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.extend(output.take_definitions(true));
|
||||
doc["methods"].as_array_mut().unwrap().push(json!({
|
||||
let has_params = !params.is_empty();
|
||||
let mut method = json!({
|
||||
"name":name,
|
||||
"summary":docs.split("\n\n").next().unwrap_or_default(),
|
||||
"description":docs,
|
||||
"paramStructure":"by-name",
|
||||
"params":params,
|
||||
"result":{"name":"result","schema":result_schema},
|
||||
"x-sausage-params":params_ref,
|
||||
}));
|
||||
});
|
||||
if has_params {
|
||||
method["x-sausage-params"] = params_ref;
|
||||
}
|
||||
doc["methods"].as_array_mut().unwrap().push(method);
|
||||
}
|
||||
|
||||
/// Resolve shared schema names after all methods contribute their contracts.
|
||||
|
||||
@@ -33,10 +33,6 @@
|
||||
"text"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"Empty": {
|
||||
"description": "Empty parameters for methods without arguments.",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -56,10 +52,61 @@
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"summary": "Publish a control endpoint until EOF.",
|
||||
"x-sausage-params": {
|
||||
"$ref": "#/components/schemas/Empty"
|
||||
}
|
||||
"summary": "Publish a control endpoint until EOF."
|
||||
},
|
||||
{
|
||||
"description": "Forward detached progress through the real core.",
|
||||
"name": "relay",
|
||||
"paramStructure": "by-name",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "result",
|
||||
"schema": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"summary": "Forward detached progress through the real core."
|
||||
},
|
||||
{
|
||||
"description": "Send progress before the final result.",
|
||||
"name": "progress",
|
||||
"paramStructure": "by-name",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "result",
|
||||
"schema": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"summary": "Send progress before the final result."
|
||||
},
|
||||
{
|
||||
"description": "Start a detached service that survives this caller.",
|
||||
"name": "launch",
|
||||
"paramStructure": "by-name",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "result",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"summary": "Start a detached service that survives this caller."
|
||||
},
|
||||
{
|
||||
"description": "Keep the process alive until the core closes stdin.",
|
||||
"name": "service",
|
||||
"paramStructure": "by-name",
|
||||
"params": [],
|
||||
"result": {
|
||||
"name": "result",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"summary": "Keep the process alive until the core closes stdin."
|
||||
},
|
||||
{
|
||||
"description": "Add two numbers.\n\nThe result is their sum.",
|
||||
@@ -136,10 +183,7 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"summary": "Return an internal serialization error.",
|
||||
"x-sausage-params": {
|
||||
"$ref": "#/components/schemas/Empty"
|
||||
}
|
||||
"summary": "Return an internal serialization error."
|
||||
},
|
||||
{
|
||||
"description": "Return a deliberate error.",
|
||||
@@ -152,10 +196,7 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"summary": "Return a deliberate error.",
|
||||
"x-sausage-params": {
|
||||
"$ref": "#/components/schemas/Empty"
|
||||
}
|
||||
"summary": "Return a deliberate error."
|
||||
},
|
||||
{
|
||||
"description": "Exercise calls, notifications, and advisory locks.",
|
||||
@@ -168,10 +209,7 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"summary": "Exercise calls, notifications, and advisory locks.",
|
||||
"x-sausage-params": {
|
||||
"$ref": "#/components/schemas/Empty"
|
||||
}
|
||||
"summary": "Exercise calls, notifications, and advisory locks."
|
||||
},
|
||||
{
|
||||
"description": "Drop an acquisition before the core grants its lock.",
|
||||
@@ -184,10 +222,7 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"summary": "Drop an acquisition before the core grants its lock.",
|
||||
"x-sausage-params": {
|
||||
"$ref": "#/components/schemas/Empty"
|
||||
}
|
||||
"summary": "Drop an acquisition before the core grants its lock."
|
||||
},
|
||||
{
|
||||
"description": "Wait until EOF, then run the stop hook.",
|
||||
@@ -200,10 +235,7 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"summary": "Wait until EOF, then run the stop hook.",
|
||||
"x-sausage-params": {
|
||||
"$ref": "#/components/schemas/Empty"
|
||||
}
|
||||
"summary": "Wait until EOF, then run the stop hook."
|
||||
},
|
||||
{
|
||||
"description": "Keep the process alive with a background call.",
|
||||
@@ -216,10 +248,7 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"summary": "Keep the process alive with a background call.",
|
||||
"x-sausage-params": {
|
||||
"$ref": "#/components/schemas/Empty"
|
||||
}
|
||||
"summary": "Keep the process alive with a background call."
|
||||
}
|
||||
],
|
||||
"openrpc": "1.2.6",
|
||||
@@ -239,10 +268,7 @@
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"summary": "Report progress and return a stable counter.",
|
||||
"x-sausage-params": {
|
||||
"$ref": "#/components/schemas/Empty"
|
||||
}
|
||||
"summary": "Report progress and return a stable counter."
|
||||
},
|
||||
{
|
||||
"description": "End the endpoint process after the response.",
|
||||
@@ -255,10 +281,7 @@
|
||||
"type": "null"
|
||||
}
|
||||
},
|
||||
"summary": "End the endpoint process after the response.",
|
||||
"x-sausage-params": {
|
||||
"$ref": "#/components/schemas/Empty"
|
||||
}
|
||||
"summary": "End the endpoint process after the response."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,6 +11,23 @@ impl Consumer {
|
||||
.on_notify(|name, _| eprintln!("typed progress: {name}"))
|
||||
.await? as i32)
|
||||
}
|
||||
/// Connect to a world endpoint from the real core.
|
||||
async fn world_endpoint(&self) -> Result<u64> {
|
||||
let control = probe::control::connect("/state/control").await?;
|
||||
let received = std::rc::Rc::new(std::cell::Cell::new(false));
|
||||
let progress = received.clone();
|
||||
let result = control
|
||||
.stats()
|
||||
.on_notify(move |name, value| {
|
||||
progress.set(name == "endpoint-progress" && value["value"] == 7);
|
||||
})
|
||||
.await?;
|
||||
assert!(
|
||||
received.get(),
|
||||
"the endpoint must deliver progress before its result"
|
||||
);
|
||||
Ok(result)
|
||||
}
|
||||
/// Connect to the provider's endpoint and receive its notification.
|
||||
async fn endpoint(&self) -> Result<u64> {
|
||||
let control = probe::control::connect("/endpoint").await?;
|
||||
|
||||
@@ -28,6 +28,32 @@ impl Probe {
|
||||
async fn serve(&self) -> Result<()> {
|
||||
sausage_plugin::listen("/endpoint", std::rc::Rc::new(Control)).await
|
||||
}
|
||||
/// Forward detached progress through the real core.
|
||||
async fn relay(&self) -> Result<i32> {
|
||||
call("probe.progress", json!({}))
|
||||
.detach()
|
||||
.on_notify(move |name, params| {
|
||||
Ctx::default().notify(name, params).unwrap();
|
||||
})
|
||||
.await
|
||||
.map(|value| value.as_i64().unwrap() as i32)
|
||||
}
|
||||
/// Send progress before the final result.
|
||||
fn progress(&self, ctx: &Ctx) -> Result<i32> {
|
||||
ctx.notify("step", json!({"done":true}))?;
|
||||
Ok(42)
|
||||
}
|
||||
/// Start a detached service that survives this caller.
|
||||
async fn launch(&self) -> Result<String> {
|
||||
Ok(sausage_plugin::serde_json::from_value(
|
||||
call("probe.service", json!({})).detach().await?,
|
||||
)?)
|
||||
}
|
||||
/// Keep the process alive until the core closes stdin.
|
||||
async fn service(&self) -> Result<String> {
|
||||
sausage_plugin::listen("/state/control", std::rc::Rc::new(Control)).await?;
|
||||
Ok(sausage_plugin::pid())
|
||||
}
|
||||
/// Add two numbers.
|
||||
///
|
||||
/// The result is their sum.
|
||||
|
||||
@@ -49,6 +49,14 @@ impl Plugin {
|
||||
#[test]
|
||||
fn discovery_and_dispatch() {
|
||||
let doc = Plugin.discover();
|
||||
assert!(doc["components"]["schemas"].get("Empty").is_none());
|
||||
let choice = doc["methods"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|m| m["name"] == "choice")
|
||||
.unwrap();
|
||||
assert!(choice.get("x-sausage-params").is_none());
|
||||
assert!(
|
||||
doc["x-sausage-endpoints"]["control"]["methods"]
|
||||
.as_array()
|
||||
|
||||
Reference in New Issue
Block a user