use std::collections::BTreeMap;
use std::fmt::Write as _;
use schemars::{schema_for, JsonSchema, Schema};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use crate::capability::{Capability, CAPABILITIES};
use crate::envelope::Envelope;
use crate::filter::{Filter, Matcher};
use crate::schema::{Registry, SchemaId};
use crate::vocabulary::{Reserved, Vocabulary};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum Format {
#[default]
Json,
Text,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum Lang {
Json,
Rust,
Python,
Typescript,
}
impl Lang {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Lang::Json => "json",
Lang::Rust => "rust",
Lang::Python => "python",
Lang::Typescript => "typescript",
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SchemaListOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<Format>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SchemaCheckOptions {
pub id: SchemaId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SchemaGenOptions {
pub id: SchemaId,
pub lang: Lang,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SchemaRegisterOptions {
pub id: SchemaId,
pub file: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registry: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct EventsMergeOptions {
pub files: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub filter: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<Format>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct EventsEmitOptions {
pub path: String,
pub kind: String,
pub stream: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub labels: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub format: Option<Format>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(try_from = "SchemaEntryWire")]
pub struct SchemaEntry {
pub id: SchemaId,
pub family: String,
pub version: u32,
}
#[derive(Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
#[schemars(rename = "SchemaEntry")]
struct SchemaEntryWire {
id: SchemaId,
family: String,
version: u32,
}
impl TryFrom<SchemaEntryWire> for SchemaEntry {
type Error = String;
fn try_from(wire: SchemaEntryWire) -> Result<Self, Self::Error> {
let entry = Self::from(&wire.id);
if wire.family != entry.family || wire.version != entry.version {
return Err(format!(
"the schema entry for {} restates it as family {} at version {}, which its id contradicts",
wire.id, wire.family, wire.version
));
}
Ok(entry)
}
}
impl From<&SchemaId> for SchemaEntry {
fn from(id: &SchemaId) -> Self {
Self {
id: id.clone(),
family: id.family(),
version: id.version(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct RegistryDocument {
pub id: SchemaId,
pub schema: Value,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
pub struct VocabularyManifest {
pub name: &'static str,
pub reserved: &'static [Reserved],
pub dimensions: &'static [Reserved],
pub default_source: &'static str,
}
#[derive(Debug, Serialize)]
pub struct Bundle {
pub capabilities: &'static [Capability],
pub vocabulary: VocabularyManifest,
pub envelope: Schema,
pub filter: Schema,
pub matcher: Schema,
pub schema_id: Schema,
pub registry_document: Schema,
pub schema_list: Schema,
pub options: BTreeMap<&'static str, Schema>,
pub messages: BTreeMap<String, Value>,
}
#[must_use]
pub fn bundle<V: Vocabulary>(registry: &Registry) -> Bundle {
let mut options = BTreeMap::new();
options.insert("schema_list_options", schema_for!(SchemaListOptions));
options.insert("schema_check_options", schema_for!(SchemaCheckOptions));
options.insert("schema_gen_options", schema_for!(SchemaGenOptions));
options.insert(
"schema_register_options",
schema_for!(SchemaRegisterOptions),
);
options.insert("events_merge_options", schema_for!(EventsMergeOptions));
options.insert("events_emit_options", schema_for!(EventsEmitOptions));
Bundle {
capabilities: CAPABILITIES,
vocabulary: VocabularyManifest {
name: V::NAME,
reserved: V::RESERVED,
dimensions: V::DIMENSIONS,
default_source: V::DEFAULT_SOURCE,
},
envelope: schema_for!(Envelope<V>),
filter: schema_for!(Filter<V>),
matcher: schema_for!(Matcher<V>),
schema_id: schema_for!(SchemaId),
registry_document: schema_for!(RegistryDocument),
schema_list: schema_for!(Vec<SchemaEntry>),
options,
messages: registry
.ids()
.into_iter()
.filter_map(|id| {
registry
.schema(&id)
.map(|document| (id.to_string(), document.clone()))
})
.collect(),
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum GenerateError {
#[error("{id}: `{lang}` declarations are generated by the {lang} SDK package, not by this build; ask for `json` or `rust`")]
Unsupported {
id: SchemaId,
lang: &'static str,
},
#[error("{id}: cannot render {at} as Rust: {why}")]
Unrenderable {
id: SchemaId,
at: String,
why: String,
},
}
pub fn generate(lang: Lang, id: &SchemaId, document: &Value) -> Result<String, GenerateError> {
match lang {
Lang::Json => Ok(format!(
"{}\n",
serde_json::to_string_pretty(document).unwrap_or_default()
)),
Lang::Rust => rust::render(id, document),
Lang::Python | Lang::Typescript => Err(GenerateError::Unsupported {
id: id.clone(),
lang: lang.as_str(),
}),
}
}
mod rust {
use super::{GenerateError, Map, SchemaId, Value};
use std::collections::BTreeMap;
use std::fmt::Write as _;
pub(super) fn render(id: &SchemaId, document: &Value) -> Result<String, GenerateError> {
let mut out = String::new();
let _ = writeln!(
out,
"// Generated by `onemessagebus schema gen --lang rust {id}`; regenerates the same document."
);
let _ = writeln!(out, "// Do not edit by hand.");
let title = document
.get("title")
.and_then(Value::as_str)
.ok_or_else(|| unrenderable(id, "", "the document has no title to name the type by"))?;
let mut types = Names::new("type");
let ident = types.admit(id, "/title", "title", title, title.to_owned())?;
let mut declarations = vec![(title, ident, String::new(), document)];
if let Some(defs) = document.get("$defs").and_then(Value::as_object) {
for (name, def) in defs {
let at = format!("/$defs/{}", pointer_token(name));
let ident = types.admit(id, &at, "$defs name", name, name.clone())?;
declarations.push((name, ident, at, def));
}
}
for (name, ident, at, schema) in declarations {
out.push('\n');
declaration(id, name, &ident, &at, schema, &mut out)?;
}
Ok(out)
}
const RAW_KEYWORDS: &[&str] = &[
"abstract", "as", "async", "await", "become", "box", "break", "const", "continue", "do",
"dyn", "else", "enum", "extern", "false", "final", "fn", "for", "gen", "if", "impl", "in",
"let", "loop", "macro", "match", "mod", "move", "mut", "override", "priv", "pub", "ref",
"return", "static", "struct", "trait", "true", "try", "type", "typeof", "unsafe",
"unsized", "use", "virtual", "where", "while", "yield",
];
const UNRAW_KEYWORDS: &[&str] = &["crate", "self", "Self", "super"];
struct Names<'a> {
declares: &'static str,
taken: BTreeMap<String, Option<(&'a str, &'a str)>>,
}
impl<'a> Names<'a> {
fn new(declares: &'static str) -> Self {
Self {
declares,
taken: BTreeMap::new(),
}
}
fn reserve(&mut self, ident: &str) {
self.taken.insert(ident.to_owned(), None);
}
fn admit(
&mut self,
id: &SchemaId,
at: &str,
what: &'a str,
name: &'a str,
derived: String,
) -> Result<String, GenerateError> {
let ident = identifier(id, at, what, name, derived)?;
match self.taken.get(&ident) {
Some(Some((named, earlier))) => Err(unrenderable(
id,
at,
&format!(
"the {named} {earlier:?} and the {what} {name:?} both become the {} `{ident}`",
self.declares
),
)),
Some(None) => Err(unrenderable(
id,
at,
&format!(
"the {what} {name:?} becomes `{ident}`, the {} the renderer declares for every other key",
self.declares
),
)),
None => {
self.taken.insert(ident.clone(), Some((what, name)));
Ok(ident)
}
}
}
}
fn identifier(
id: &SchemaId,
at: &str,
what: &str,
name: &str,
derived: String,
) -> Result<String, GenerateError> {
let Some(defect) = defect_of(name, &derived) else {
return Ok(if RAW_KEYWORDS.contains(&derived.as_str()) {
format!("r#{derived}")
} else {
derived
});
};
let spelled = if derived == name || derived.is_empty() {
String::new()
} else {
format!(" (as `{derived}`)")
};
Err(unrenderable(
id,
at,
&format!("the {what} {name:?}{spelled} is not a Rust identifier: {defect}"),
))
}
fn defect_of(name: &str, ident: &str) -> Option<String> {
let Some(first) = ident.chars().next() else {
return Some(if name.is_empty() {
"it is empty".to_owned()
} else {
"nothing of it is left once mapped".to_owned()
});
};
if first.is_ascii_digit() {
return Some("it starts with a digit".to_owned());
}
if let Some(ch) = ident
.chars()
.find(|ch| !ch.is_ascii_alphanumeric() && *ch != '_')
{
return Some(format!(
"{ch:?} is not an ASCII letter, digit or underscore"
));
}
if ident == "_" {
return Some("a lone `_` is a pattern, not a name".to_owned());
}
if UNRAW_KEYWORDS.contains(&ident) {
return Some(format!(
"`{ident}` is a keyword Rust does not take even as a raw identifier"
));
}
None
}
fn pointer_token(name: &str) -> String {
name.replace('~', "~0").replace('/', "~1")
}
fn unrenderable(id: &SchemaId, at: &str, why: &str) -> GenerateError {
GenerateError::Unrenderable {
id: id.clone(),
at: if at.is_empty() {
"the document".to_owned()
} else {
at.to_owned()
},
why: why.to_owned(),
}
}
fn doc(description: Option<&Value>, indent: &str, out: &mut String) {
if let Some(text) = description.and_then(Value::as_str) {
for line in text.lines() {
let _ = writeln!(
out,
"{indent}///{}{line}",
if line.is_empty() { "" } else { " " }
);
}
}
}
fn declaration(
id: &SchemaId,
name: &str,
ident: &str,
at: &str,
schema: &Value,
out: &mut String,
) -> Result<(), GenerateError> {
doc(schema.get("description"), "", out);
if let Some(variants) = words_of(schema) {
let _ = writeln!(
out,
"#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]"
);
let _ = writeln!(out, "pub enum {ident} {{");
let mut taken = Names::new("variant");
for (word, description, within) in variants {
let word = word
.as_str()
.ok_or_else(|| unrenderable(id, name, "an enum value that is not a string"))?;
let variant = taken.admit(
id,
&format!("{at}/{within}"),
"enum value",
word,
pascal(word),
)?;
doc(description, " ", out);
let _ = writeln!(out, " #[serde(rename = \"{}\")]", escape(word));
let _ = writeln!(out, " {variant},");
}
let _ = writeln!(out, "}}");
return Ok(());
}
let properties = schema
.get("properties")
.and_then(Value::as_object)
.ok_or_else(|| {
unrenderable(
id,
name,
"a schema that is neither an object with properties nor a string enum",
)
})?;
let required: Vec<&str> = schema
.get("required")
.and_then(Value::as_array)
.map(|required| required.iter().filter_map(Value::as_str).collect())
.unwrap_or_default();
let closed = schema.get("additionalProperties") == Some(&Value::Bool(false));
let flattened = !closed
&& schema
.get("additionalProperties")
.is_some_and(|extra| extra != &Value::Bool(false));
let _ = writeln!(
out,
"#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]"
);
if closed {
let _ = writeln!(out, "#[serde(deny_unknown_fields)]");
}
let _ = writeln!(out, "pub struct {ident} {{");
let mut taken = Names::new("field");
if flattened {
taken.reserve("extra");
}
for (property, property_schema) in properties {
let field = taken.admit(
id,
&format!("{at}/properties/{}", pointer_token(property)),
"property name",
property,
field_name(property),
)?;
doc(property_schema.get("description"), " ", out);
let at = format!("{name}.{property}");
let (nullable, inner) = nullable(property_schema);
let ty = type_of(id, &at, &inner)?;
let is_required = required.contains(&property.as_str());
let mut attributes = Vec::new();
if field != *property {
attributes.push(format!("rename = \"{}\"", escape(property)));
}
let ty = if nullable {
if !is_required {
attributes.push("default".to_owned());
attributes.push("skip_serializing_if = \"Option::is_none\"".to_owned());
}
format!("Option<{ty}>")
} else {
if !is_required {
attributes.push("default".to_owned());
}
ty
};
if !attributes.is_empty() {
let _ = writeln!(out, " #[serde({})]", attributes.join(", "));
}
let _ = writeln!(out, " pub {field}: {ty},");
}
if flattened {
let _ = writeln!(out, " /// Every other key, carried through untouched.");
let _ = writeln!(out, " #[serde(flatten)]");
let _ = writeln!(
out,
" pub extra: serde_json::Map<String, serde_json::Value>,"
);
}
let _ = writeln!(out, "}}");
Ok(())
}
fn words_of(schema: &Value) -> Option<Vec<(&Value, Option<&Value>, String)>> {
if let Some(words) = schema.get("enum").and_then(Value::as_array) {
return Some(
words
.iter()
.enumerate()
.map(|(index, word)| (word, None, format!("enum/{index}")))
.collect(),
);
}
let branches = schema.get("oneOf")?.as_array()?;
let mut words = Vec::new();
for (index, branch) in branches.iter().enumerate() {
if branch.get("type") != Some(&Value::String("string".to_owned())) {
return None;
}
words.push((
branch.get("const")?,
branch.get("description"),
format!("oneOf/{index}/const"),
));
}
Some(words)
}
fn nullable(schema: &Value) -> (bool, Value) {
if let Some(types) = schema.get("type").and_then(Value::as_array) {
let words: Vec<&str> = types.iter().filter_map(Value::as_str).collect();
if words.contains(&"null") && words.len() == 2 {
let mut inner = schema.clone();
let other = words.iter().find(|word| **word != "null").copied();
if let (Some(other), Some(object)) = (other, inner.as_object_mut()) {
object.insert("type".to_owned(), Value::String(other.to_owned()));
}
return (true, inner);
}
}
if let Some(branches) = schema.get("anyOf").and_then(Value::as_array) {
let null = Value::Object(Map::from_iter([(
"type".to_owned(),
Value::String("null".to_owned()),
)]));
if branches.len() == 2 && branches.contains(&null) {
if let Some(other) = branches.iter().find(|branch| **branch != null) {
return (true, other.clone());
}
}
}
(false, schema.clone())
}
fn type_of(id: &SchemaId, at: &str, schema: &Value) -> Result<String, GenerateError> {
let says_nothing = schema
.as_object()
.is_some_and(|object| object.keys().all(|key| key == "description"));
if schema == &Value::Bool(true) || says_nothing {
return Ok("serde_json::Value".to_owned());
}
if let Some(reference) = schema.get("$ref").and_then(Value::as_str) {
let target = reference
.strip_prefix("#/$defs/")
.ok_or_else(|| unrenderable(id, at, "a reference outside the document's $defs"))?;
return identifier(id, at, "reference target", target, target.to_owned());
}
let kind = schema
.get("type")
.and_then(Value::as_str)
.ok_or_else(|| unrenderable(id, at, "a schema with no single type"))?;
let format = schema.get("format").and_then(Value::as_str);
Ok(match (kind, format) {
("string", _) => "String".to_owned(),
("boolean", _) => "bool".to_owned(),
("integer", Some("uint64")) => "u64".to_owned(),
("integer", Some("uint32")) => "u32".to_owned(),
("integer", Some("uint16")) => "u16".to_owned(),
("integer", Some("uint8")) => "u8".to_owned(),
("integer", Some("uint")) => "usize".to_owned(),
("integer", Some("int64")) => "i64".to_owned(),
("integer", Some("int32")) => "i32".to_owned(),
("integer", Some("int16")) => "i16".to_owned(),
("integer", Some("int8")) => "i8".to_owned(),
("integer", Some("int")) => "isize".to_owned(),
("integer", _) => "i64".to_owned(),
("number", Some("float")) => "f32".to_owned(),
("number", _) => "f64".to_owned(),
("array", _) => {
let items = schema
.get("items")
.ok_or_else(|| unrenderable(id, at, "an array with no item schema"))?;
format!("Vec<{}>", type_of(id, at, items)?)
}
("object", _) => {
if schema.get("properties").is_some() {
return Err(unrenderable(
id,
at,
"an inline object with properties; declare it in $defs",
));
}
match schema.get("additionalProperties") {
None | Some(Value::Bool(true)) => {
"serde_json::Map<String, serde_json::Value>".to_owned()
}
Some(values) => format!(
"std::collections::BTreeMap<String, {}>",
type_of(id, at, values)?
),
}
}
(other, _) => return Err(unrenderable(id, at, &format!("a `{other}` schema"))),
})
}
fn field_name(property: &str) -> String {
let mut out = String::new();
for (index, ch) in property.chars().enumerate() {
if ch.is_ascii_uppercase() {
if index > 0 {
out.push('_');
}
out.push(ch.to_ascii_lowercase());
} else if ch == '-' {
out.push('_');
} else {
out.push(ch);
}
}
out
}
fn pascal(word: &str) -> String {
let mut out = String::new();
let mut upper = true;
for ch in word.chars() {
if ch == '-' || ch == '_' || ch == ' ' {
upper = true;
} else if upper {
out.push(ch.to_ascii_uppercase());
upper = false;
} else {
out.push(ch);
}
}
out
}
fn escape(text: &str) -> String {
text.replace('\\', "\\\\").replace('"', "\\\"")
}
}
impl Bundle {
#[must_use]
pub fn to_json(&self) -> String {
let mut out = serde_json::to_string_pretty(self).unwrap_or_default();
let _ = writeln!(out);
out
}
}