#[cfg(feature = "idl")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::ty::Prim;
pub const CURRENT_SCHEMA_VERSION: &str = "0.11.0";
pub const SUPPORTED_VERSIONS: &[&str] = &[CURRENT_SCHEMA_VERSION];
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_false(b: &bool) -> bool {
!*b
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
#[cfg_attr(
feature = "idl",
schemars(description = "Top-level WeaveFFI API definition.")
)]
pub struct Api {
pub version: String,
pub modules: Vec<Module>,
}
impl Api {
pub fn for_each_type_ref<'a>(&'a self, f: &mut dyn FnMut(&'a TypeRef)) {
fn functions<'a>(fs: &'a [Function], f: &mut dyn FnMut(&'a TypeRef)) {
for func in fs {
for p in &func.params {
f(&p.ty);
}
if let Some(r) = &func.returns {
f(r);
}
}
}
fn fields<'a>(fields: &'a [StructField], f: &mut dyn FnMut(&'a TypeRef)) {
for field in fields {
f(&field.ty);
}
}
fn modules<'a>(ms: &'a [Module], f: &mut dyn FnMut(&'a TypeRef)) {
for m in ms {
functions(&m.functions, f);
for i in &m.interfaces {
functions(&i.constructors, f);
functions(&i.methods, f);
functions(&i.statics, f);
}
for c in &m.callback_interfaces {
functions(&c.methods, f);
}
for s in &m.structs {
fields(&s.fields, f);
}
for v in m.enums.iter().flat_map(|e| &e.variants) {
fields(&v.fields, f);
}
for c in m.errors.iter().flat_map(|d| &d.codes) {
fields(&c.fields, f);
}
modules(&m.modules, f);
}
}
modules(&self.modules, f);
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
#[cfg_attr(
feature = "idl",
schemars(
description = "A WeaveFFI module: a named group of functions, types, callback interfaces, and errors."
)
)]
pub struct Module {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doc: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub functions: Vec<Function>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub interfaces: Vec<InterfaceDef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub callback_interfaces: Vec<CallbackInterfaceDef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub structs: Vec<StructDef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub enums: Vec<EnumDef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub errors: Option<ErrorDomain>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub modules: Vec<Module>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Function {
pub name: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub params: Vec<Param>,
#[serde(rename = "return", default, skip_serializing_if = "Option::is_none")]
pub returns: Option<TypeRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doc: Option<String>,
#[serde(default, skip_serializing_if = "is_false")]
pub throws: bool,
#[serde(default, rename = "async", skip_serializing_if = "is_false")]
pub r#async: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub cancellable: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deprecated: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
#[cfg_attr(
feature = "idl",
schemars(
description = "An interface: an opaque object type with constructors, methods, and statics."
)
)]
pub struct InterfaceDef {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doc: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deprecated: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub constructors: Vec<Function>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub methods: Vec<Function>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub statics: Vec<Function>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
#[cfg_attr(
feature = "idl",
schemars(
description = "A callback interface: a set of methods the consumer implements and the producer calls."
)
)]
pub struct CallbackInterfaceDef {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doc: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deprecated: Option<String>,
pub methods: Vec<Function>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Param {
pub name: String,
#[serde(rename = "type")]
pub ty: TypeRef,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doc: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum TypeRef {
Prim(Prim),
Named(String),
Optional(Box<TypeRef>),
List(Box<TypeRef>),
Map(Box<TypeRef>, Box<TypeRef>),
Iterator(Box<TypeRef>),
}
pub fn parse_type_ref(s: &str) -> Result<TypeRef, String> {
let s = s.trim();
if s.is_empty() {
return Err("empty type reference".to_string());
}
if s.starts_with('[') && s.ends_with(']') {
let inner = &s[1..s.len() - 1];
return parse_type_ref(inner).map(|t| match t {
TypeRef::Prim(Prim::U8) => TypeRef::Prim(Prim::Bytes),
t => TypeRef::List(Box::new(t)),
});
}
if s.starts_with('{') && s.ends_with('}') {
let inner = &s[1..s.len() - 1];
let colon =
map_separator(inner).ok_or_else(|| "map type missing ':' separator".to_string())?;
let key = parse_type_ref(&inner[..colon])?;
let val = parse_type_ref(&inner[colon + 1..])?;
return Ok(TypeRef::Map(Box::new(key), Box::new(val)));
}
if let Some(inner) = s.strip_suffix('?') {
return parse_type_ref(inner).map(|t| TypeRef::Optional(Box::new(t)));
}
if let Some(inner) = s
.strip_prefix("iter<")
.and_then(|rest| rest.strip_suffix('>'))
{
return parse_type_ref(inner).map(|t| TypeRef::Iterator(Box::new(t)));
}
Ok(Prim::from_name(s).map_or_else(|| TypeRef::Named(s.to_string()), TypeRef::Prim))
}
fn map_separator(inner: &str) -> Option<usize> {
let mut depth = 0usize;
for (i, ch) in inner.char_indices() {
match ch {
'[' | '{' | '<' => depth += 1,
']' | '}' | '>' => depth = depth.saturating_sub(1),
':' if depth == 0 => return Some(i),
_ => {}
}
}
None
}
impl TypeRef {
pub fn user_name(&self) -> Option<&str> {
match self {
TypeRef::Named(n) => Some(n),
_ => None,
}
}
pub fn walk<'a>(&'a self, f: &mut dyn FnMut(&'a TypeRef)) {
f(self);
match self {
TypeRef::Optional(inner) | TypeRef::List(inner) | TypeRef::Iterator(inner) => {
inner.walk(f);
}
TypeRef::Map(k, v) => {
k.walk(f);
v.walk(f);
}
_ => {}
}
}
}
fn type_ref_to_string(ty: &TypeRef) -> String {
match ty {
TypeRef::Prim(p) => p.snake().to_string(),
TypeRef::Named(name) => name.clone(),
TypeRef::Optional(inner) => format!("{}?", type_ref_to_string(inner)),
TypeRef::List(inner) => format!("[{}]", type_ref_to_string(inner)),
TypeRef::Map(k, v) => format!("{{{}:{}}}", type_ref_to_string(k), type_ref_to_string(v)),
TypeRef::Iterator(inner) => format!("iter<{}>", type_ref_to_string(inner)),
}
}
impl std::fmt::Display for TypeRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&type_ref_to_string(self))
}
}
impl Serialize for TypeRef {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&type_ref_to_string(self))
}
}
impl<'de> Deserialize<'de> for TypeRef {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
parse_type_ref(&s).map_err(serde::de::Error::custom)
}
}
#[cfg(feature = "idl")]
impl JsonSchema for TypeRef {
fn schema_name() -> String {
"TypeRef".to_string()
}
fn schema_id() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed(concat!(module_path!(), "::TypeRef"))
}
fn json_schema(_generator: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
let mut schema = schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
..Default::default()
};
let meta = schema.metadata();
meta.title = Some("TypeRef".to_string());
meta.description = Some(
"Reference to a type. Encoded as a string with custom syntax: \
primitives (`i8`, `i16`, `i32`, `i64`, `u8`, `u16`, `u32`, `u64`, \
`f32`, `f64`, `bool`, `string`, `bytes`), iterators (`iter<{T}>`), \
lists (`[{T}]`), maps (`{{K:V}}`), optionals (`{T}?`), or the bare \
(unqualified) name of a user-defined struct, enum, interface, or callback \
interface."
.to_string(),
);
schema.into()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
#[cfg_attr(
feature = "idl",
schemars(
description = "An enum type. C-style when every variant is a bare discriminant; an algebraic sum type when any variant declares fields."
)
)]
pub struct EnumDef {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doc: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deprecated: Option<String>,
pub variants: Vec<EnumVariant>,
}
impl EnumDef {
pub fn is_rich(&self) -> bool {
self.variants.iter().any(|v| !v.fields.is_empty())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct EnumVariant {
pub name: String,
pub value: i32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doc: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub fields: Vec<StructField>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
#[cfg_attr(
feature = "idl",
schemars(description = "A struct (record) type with named fields.")
)]
pub struct StructDef {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doc: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub deprecated: Option<String>,
pub fields: Vec<StructField>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct StructField {
pub name: String,
#[serde(rename = "type")]
pub ty: TypeRef,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doc: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct ErrorDomain {
pub name: String,
pub codes: Vec<ErrorCode>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "idl", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct ErrorCode {
pub name: String,
pub code: i32,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doc: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub fields: Vec<StructField>,
}
#[cfg(test)]
mod tests {
use super::*;
fn rt(s: &str) -> TypeRef {
let ty = parse_type_ref(s).unwrap_or_else(|e| panic!("{s}: {e}"));
assert_eq!(ty.to_string(), s, "type syntax must round-trip");
ty
}
#[test]
fn primitives_round_trip() {
let cases: &[(&str, TypeRef)] = &[
("i8", TypeRef::Prim(Prim::I8)),
("i16", TypeRef::Prim(Prim::I16)),
("i32", TypeRef::Prim(Prim::I32)),
("i64", TypeRef::Prim(Prim::I64)),
("u8", TypeRef::Prim(Prim::U8)),
("u16", TypeRef::Prim(Prim::U16)),
("u32", TypeRef::Prim(Prim::U32)),
("u64", TypeRef::Prim(Prim::U64)),
("f32", TypeRef::Prim(Prim::F32)),
("f64", TypeRef::Prim(Prim::F64)),
("bool", TypeRef::Prim(Prim::Bool)),
("string", TypeRef::Prim(Prim::String)),
("bytes", TypeRef::Prim(Prim::Bytes)),
];
for (s, expected) in cases {
assert_eq!(&rt(s), expected);
}
}
#[test]
fn composites_round_trip() {
assert_eq!(rt("Contact"), TypeRef::Named("Contact".into()));
assert_eq!(rt("shared.Status"), TypeRef::Named("shared.Status".into()));
assert_eq!(
rt("Contact?"),
TypeRef::Optional(Box::new(TypeRef::Named("Contact".into())))
);
assert_eq!(
rt("[string]"),
TypeRef::List(Box::new(TypeRef::Prim(Prim::String)))
);
assert_eq!(
rt("{string:i32}"),
TypeRef::Map(
Box::new(TypeRef::Prim(Prim::String)),
Box::new(TypeRef::Prim(Prim::I32))
)
);
assert_eq!(
rt("iter<Contact>"),
TypeRef::Iterator(Box::new(TypeRef::Named("Contact".into())))
);
assert_eq!(
rt("[{string:[i32?]}]"),
TypeRef::List(Box::new(TypeRef::Map(
Box::new(TypeRef::Prim(Prim::String)),
Box::new(TypeRef::List(Box::new(TypeRef::Optional(Box::new(
TypeRef::Prim(Prim::I32)
)))))
)))
);
}
#[test]
fn u8_list_canonicalizes_to_bytes() {
assert_eq!(parse_type_ref("[u8]").unwrap(), TypeRef::Prim(Prim::Bytes));
assert_eq!(
parse_type_ref("[[u8]]").unwrap(),
TypeRef::List(Box::new(TypeRef::Prim(Prim::Bytes)))
);
}
#[test]
fn map_separator_skips_nested_colons() {
assert_eq!(
parse_type_ref("{{string:i32}:bool}").unwrap(),
TypeRef::Map(
Box::new(TypeRef::Map(
Box::new(TypeRef::Prim(Prim::String)),
Box::new(TypeRef::Prim(Prim::I32))
)),
Box::new(TypeRef::Prim(Prim::Bool))
)
);
}
#[test]
fn malformed_types_are_errors() {
assert!(parse_type_ref("").is_err());
assert!(parse_type_ref(" ").is_err());
assert!(parse_type_ref("{string}").is_err());
assert!(parse_type_ref("[]").is_err());
assert!(parse_type_ref("?").is_err());
}
#[test]
fn walk_visits_nested_types_outermost_first() {
let ty = parse_type_ref("{string:[Contact?]}").unwrap();
let mut seen = Vec::new();
ty.walk(&mut |t| seen.push(t.to_string()));
assert_eq!(
seen,
[
"{string:[Contact?]}",
"string",
"[Contact?]",
"Contact?",
"Contact"
]
);
}
#[test]
fn document_round_trips_through_yaml_and_json() {
let yaml = r#"
version: "0.11.0"
modules:
- name: contacts
doc: Address book
structs:
- name: Contact
deprecated: use Person
fields:
- { name: name, type: string }
- { name: tags, type: "[string]" }
- { name: book, type: "Book?" }
enums:
- name: Shape
variants:
- { name: Circle, value: 0, fields: [{ name: r, type: f64 }] }
- { name: Dot, value: 1 }
interfaces:
- name: Book
constructors:
- { name: open, params: [{ name: path, type: string }], throws: true }
methods:
- { name: find, params: [{ name: q, type: string }], return: "Contact?" }
- { name: watch, params: [{ name: listener, type: ChangeListener }] }
callback_interfaces:
- name: ChangeListener
methods:
- { name: on_change, params: [{ name: id, type: i64 }] }
- { name: should_continue, params: [], return: bool }
errors:
name: BookError
codes:
- { name: NotFound, code: 1, message: missing, fields: [{ name: id, type: i64 }] }
functions:
- name: count
params: []
return: i64
async: true
cancellable: true
deprecated: gone
modules:
- name: inner
functions:
- { name: ping, params: [], return: string }
"#;
let api: Api = serde_yaml::from_str(yaml).unwrap();
assert_eq!(api.version, CURRENT_SCHEMA_VERSION);
let m = &api.modules[0];
assert_eq!(m.doc.as_deref(), Some("Address book"));
assert_eq!(m.structs[0].deprecated.as_deref(), Some("use Person"));
assert!(m.enums[0].is_rich());
assert_eq!(
m.interfaces[0].methods[0].returns,
Some(TypeRef::Optional(Box::new(TypeRef::Named(
"Contact".into()
))))
);
assert_eq!(m.callback_interfaces[0].methods.len(), 2);
assert_eq!(m.errors.as_ref().unwrap().codes[0].fields.len(), 1);
assert!(m.functions[0].r#async && m.functions[0].cancellable);
assert_eq!(m.modules[0].functions[0].name, "ping");
let json = serde_json::to_string(&api).unwrap();
let back: Api = serde_json::from_str(&json).unwrap();
assert_eq!(back, api);
let yaml2 = serde_yaml::to_string(&api).unwrap();
let back2: Api = serde_yaml::from_str(&yaml2).unwrap();
assert_eq!(back2, api);
}
#[test]
fn serialization_omits_defaulted_fields() {
let api = Api {
version: CURRENT_SCHEMA_VERSION.into(),
modules: vec![Module {
name: "m".into(),
doc: None,
functions: vec![Function {
name: "f".into(),
params: vec![],
returns: None,
doc: None,
throws: false,
r#async: false,
cancellable: false,
deprecated: None,
}],
interfaces: vec![],
callback_interfaces: vec![],
structs: vec![],
enums: vec![],
errors: None,
modules: vec![],
}],
};
let yaml = serde_yaml::to_string(&api).unwrap();
for noise in ["null", "[]", "false", "async", "throws", "doc", "params"] {
assert!(
!yaml.contains(noise),
"{noise} leaked into canonical form:\n{yaml}"
);
}
}
#[cfg(feature = "idl")]
#[test]
fn json_schema_types_are_strings() {
let schema = schemars::schema_for!(Api);
let json = serde_json::to_value(&schema).unwrap();
let type_ref = &json["definitions"]["TypeRef"];
assert_eq!(type_ref["type"], "string");
assert!(json["properties"].get("package").is_none());
assert!(json["properties"].get("generators").is_none());
assert!(json["definitions"].get("CallbackInterfaceDef").is_some());
}
}