//! 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(endpoints(control=Plugin))] impl Plugin { /// Return the same named object without a contract change. fn identity(&self, p: Add) -> Result { Ok(p) } /// Choose the first value. fn choice(&self) -> Result { Ok(Choice::First) } /// Round-trip the defaulted object. fn defaults(&self, p: Defaulted) -> Result { Ok(p) } /// Add integers. /// /// Return their sum. fn add(&self, p: Add) -> Result { Ok(p.a + p.b) } } #[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() .unwrap() .iter() .any(|m| m["name"] == "add") ); assert!(doc["components"]["schemas"].get("AddResult").is_none()); assert_eq!( doc["methods"] .as_array() .unwrap() .iter() .find(|m| m["name"] == "identity") .unwrap()["result"]["schema"]["$ref"], "#/components/schemas/Add" ); assert!( doc["components"]["schemas"]["Defaulted"] .get("required") .is_none() ); assert_eq!( doc["components"]["schemas"]["DefaultedResult"]["required"], json!(["value"]) ); assert_eq!( doc["components"]["schemas"]["Choice"]["oneOf"][0]["description"], json!("The first choice.") ); 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_eq!( sausage_plugin::block_on(Plugin.dispatch("add", json!({}), &Ctx::default())) .unwrap_err() .code, -32602 ); let internal: sausage_plugin::Error = sausage_plugin::serde_json::from_str::("bad") .unwrap_err() .into(); assert_eq!(internal.code, -32603); } #[sausage_plugin::test] async fn params_must_be_objects() { for (params, kind) in [ (json!("{\"a\":2,\"b\":3}"), "a string"), (json!([]), "an array"), (json!(42), "a number"), (json!(null), "null"), (json!(true), "a boolean"), ] { let message = format!( "params must be a JSON object, got {kind}. Send the object itself, not a JSON-encoded string." ); let outgoing = sausage_plugin::call("plugin.add", params.clone()) .await .unwrap_err(); assert_eq!(outgoing.code, -32602); assert_eq!(outgoing.message, message); for method in ["add", "choice", "unknown"] { let incoming = Plugin .dispatch(method, params.clone(), &Ctx::default()) .await .unwrap_err(); assert_eq!(incoming.code, -32602); assert_eq!(incoming.message, message); } } assert_eq!( Plugin .dispatch("add", json!({"a":2,"b":3}), &Ctx::default()) .await .unwrap(), 5 ); assert_eq!( Plugin .dispatch("defaults", json!({}), &Ctx::default()) .await .unwrap(), json!({"value":""}) ); // An object reaches transport validation rather than the params rejection. let outgoing = sausage_plugin::call("plugin.add", json!({"a":2,"b":3})) .await .unwrap_err(); assert!(outgoing.message.contains("no core transport")); } #[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"); }