sausage-plugin part 2: import! with typed clients (sausage_interface::client_rs), TCP endpoints with rpc.endpoint, examples
Part 1 fixes: complete stdout writes, buffered stdin, Rust type names in schemas, enum variant docs, -32603 for serde errors in handlers, crate docs and modules. End-to-end check of a generated consumer through a fake core.
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
//! Schema contracts and OpenRPC document generation.
|
||||
use crate::*;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// 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> {}
|
||||
|
||||
/// Create an OpenRPC document builder for generated interfaces.
|
||||
#[doc(hidden)]
|
||||
pub fn document(title: &str, version: &str) -> Value {
|
||||
let mut doc = sausage_interface::Document::new(title, version).into_value();
|
||||
doc["x-sdk-input"] = json!({});
|
||||
doc["x-sdk-output"] = json!({});
|
||||
doc
|
||||
}
|
||||
|
||||
/// Add a method with independent deserialize and serialize contracts.
|
||||
#[doc(hidden)]
|
||||
pub fn method<P: Params, R: SchemaType>(doc: &mut Value, name: &str, docs: &str) {
|
||||
fn generator(serialize: bool) -> schemars::SchemaGenerator {
|
||||
let mut settings = schemars::generate::SchemaSettings::draft07();
|
||||
settings.definitions_path = if serialize {
|
||||
"/sdk-output"
|
||||
} else {
|
||||
"/sdk-input"
|
||||
}
|
||||
.into();
|
||||
if serialize {
|
||||
settings.for_serialize()
|
||||
} else {
|
||||
settings.for_deserialize()
|
||||
}
|
||||
.into_generator()
|
||||
}
|
||||
let mut input = generator(false);
|
||||
let params_ref = serde_json::to_value(input.subschema_for::<P>()).unwrap();
|
||||
let definitions = input.take_definitions(true);
|
||||
let schema = &definitions[P::schema_name().as_ref()];
|
||||
let params:Vec<_>=schema["properties"].as_object().into_iter().flatten().map(|(name,field)| {
|
||||
json!({
|
||||
"name": name,
|
||||
"description": field.get("description").cloned().unwrap_or(Value::Null),
|
||||
"required": schema["required"].as_array().is_some_and(|required|required.contains(&json!(name))),
|
||||
"schema": field,
|
||||
})
|
||||
}).collect();
|
||||
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!({
|
||||
"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,
|
||||
}));
|
||||
}
|
||||
|
||||
/// Resolve shared schema names after all methods contribute their contracts.
|
||||
#[doc(hidden)]
|
||||
pub fn finish_document(mut doc: Value) -> Value {
|
||||
let input = doc.as_object_mut().unwrap().remove("x-sdk-input").unwrap();
|
||||
let output = doc.as_object_mut().unwrap().remove("x-sdk-output").unwrap();
|
||||
fn refs(value: &mut Value, names: &std::collections::HashMap<String, String>) {
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
if let Some(Value::String(reference)) = map.get_mut("$ref") {
|
||||
if let Some(name) = reference.strip_prefix("#/sdk-input/") {
|
||||
*reference = format!("#/components/schemas/{name}");
|
||||
} else if let Some(name) = reference.strip_prefix("#/sdk-output/") {
|
||||
*reference = format!(
|
||||
"#/components/schemas/{}",
|
||||
names.get(name).map(String::as_str).unwrap_or(name)
|
||||
);
|
||||
}
|
||||
}
|
||||
for child in map.values_mut() {
|
||||
refs(child, names);
|
||||
}
|
||||
}
|
||||
Value::Array(values) => {
|
||||
for child in values {
|
||||
refs(child, names);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let mut names = std::collections::HashMap::new();
|
||||
for (name, schema) in output.as_object().unwrap() {
|
||||
if let Some(other) = input.get(name) {
|
||||
let mut a = schema.clone();
|
||||
let mut b = other.clone();
|
||||
refs(&mut a, &names);
|
||||
refs(&mut b, &names);
|
||||
if a != b {
|
||||
names.insert(name.clone(), format!("{name}Result"));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Propagate differences through references to nested contracts.
|
||||
loop {
|
||||
let before = names.len();
|
||||
for (name, schema) in output.as_object().unwrap() {
|
||||
if let Some(other) = input.get(name) {
|
||||
let mut a = schema.clone();
|
||||
let mut b = other.clone();
|
||||
refs(&mut a, &names);
|
||||
refs(&mut b, &names);
|
||||
if a != b {
|
||||
names.insert(name.clone(), format!("{name}Result"));
|
||||
}
|
||||
}
|
||||
}
|
||||
if names.len() == before {
|
||||
break;
|
||||
}
|
||||
}
|
||||
doc["components"]["schemas"] = input;
|
||||
for (name, schema) in output.as_object().unwrap() {
|
||||
let name = names.get(name).unwrap_or(name);
|
||||
doc["components"]["schemas"]
|
||||
.as_object_mut()
|
||||
.unwrap()
|
||||
.insert(name.clone(), schema.clone());
|
||||
}
|
||||
refs(&mut doc, &names);
|
||||
for import in inventory::iter::<crate::Import> {
|
||||
doc["x-sausage-imports"][import.name] =
|
||||
serde_json::from_str(import.document).expect("validated import document");
|
||||
}
|
||||
doc
|
||||
}
|
||||
|
||||
/// Empty parameters for methods without arguments.
|
||||
#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct Empty {}
|
||||
impl SchemaType for Empty {}
|
||||
impl Params for Empty {}
|
||||
Reference in New Issue
Block a user