use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write as _;
use serde_json::Value;
use super::schema_model::{Schema, SchemaFile, TypeSet, Union};
#[derive(Debug, Clone, PartialEq, Eq)]
enum RustType {
Named(String),
Bool,
Int,
Float,
Str,
Json,
List(Box<Self>),
}
impl RustType {
fn render(&self) -> String {
match self {
Self::Named(n) => n.clone(),
Self::Bool => "bool".to_owned(),
Self::Int => "i64".to_owned(),
Self::Float => "f64".to_owned(),
Self::Str => "String".to_owned(),
Self::Json => "serde_json::Value".to_owned(),
Self::List(inner) => format!("Vec<{}>", inner.render()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Generated {
pub source: String,
pub type_names: Vec<String>,
pub dangling_refs: Vec<String>,
}
fn write_preamble(source: &mut String, file: &SchemaFile) {
let _ = writeln!(
source,
"//! Wire types for MCP revision `{}`.",
file.revision
);
let _ = writeln!(source, "//!");
let _ = writeln!(
source,
"//! GENERATED — do not edit. Produced by `generator::emit`"
);
let _ = writeln!(
source,
"//! from the vendored `spec/{}/schema.json`.",
file.revision
);
let _ = writeln!(source, "//!");
let _ = writeln!(
source,
"//! Dialect: `{}`, reference container `{}`, {} definitions.",
file.dialect,
file.ref_container.keyword(),
file.defs.len()
);
let _ = writeln!(source, "//!");
let _ = writeln!(
source,
"//! Every object type carries a flattened catch-all so unknown members"
);
let _ = writeln!(
source,
"//! survive a decode/encode round trip, which is what makes a"
);
let _ = writeln!(
source,
"//! version-translating bridge lossless. See the generator's module docs."
);
let _ = writeln!(source);
}
pub fn emit(file: &SchemaFile) -> Generated {
let mut source = String::with_capacity(64 * 1024);
write_preamble(&mut source, file);
for lint in [
"clippy::derive_partial_eq_without_eq",
"clippy::large_enum_variant",
"clippy::doc_markdown",
"clippy::doc_lazy_continuation",
"clippy::struct_field_names",
"clippy::use_self",
"rustdoc::broken_intra_doc_links",
] {
let _ = writeln!(source, "#![allow({lint})]");
}
let _ = writeln!(source);
let _ = writeln!(source, "use serde::{{Deserialize, Serialize}};");
let _ = writeln!(source);
let mut pending: Vec<(String, Union, String)> = Vec::new();
let mut multi_types: Vec<(String, String)> = Vec::new();
for (name, schema) in &file.defs {
collect_unions(schema, name, name, &mut pending);
collect_multi_types(schema, &mut multi_types);
}
multi_types.sort();
multi_types.dedup();
pending.sort_by(|a, b| a.0.cmp(&b.0));
pending.dedup_by(|a, b| a.0 == b.0);
for (ident, union, doc_name) in &pending {
let members: Vec<RustType> = union
.members
.iter()
.enumerate()
.map(|(i, m)| lower(m, &file.defs, &member_path(ident, i)))
.collect();
emit_union(
&mut source,
ident,
doc_name,
&members,
dispatch_tags(&union.members, &file.defs).as_deref(),
);
}
let _ = writeln!(
source,
"// A `\"type\": [..]` selector names a set of JSON kinds, and a value that\n\
// matches more than one of them cannot be a single Rust type. `Value`\n\
// is the honest lowering: the codec layer, not the wire type, decides\n\
// which kind it is. Each distinct set gets its own name so the\n\
// specification's own vocabulary stays visible in the generated code."
);
for (ident, names) in multi_types {
let _ = writeln!(
source,
"/// One of the JSON kinds the specification allows: {names}."
);
let _ = writeln!(source, "pub type {ident} = serde_json::Value;");
}
let _ = writeln!(source);
let mut dangling = Vec::new();
for (name, schema) in &file.defs {
if let Some(missing) = collect_dangling(schema, &file.defs) {
dangling.extend(missing);
}
let () = emit_def(&mut source, name, schema, &file.defs);
}
dangling.sort_unstable();
dangling.dedup();
Generated {
source,
type_names: file.defs.keys().cloned().collect(),
dangling_refs: dangling,
}
}
fn type_const<'a>(member: &'a Schema, defs: &'a BTreeMap<String, Schema>) -> Option<&'a str> {
let target = member.def_ref()?;
let property = defs.get(target)?.properties.get("type")?;
property.constant.as_ref()?.as_str()
}
fn dispatch_tags<'a>(
members: &'a [Box<Schema>],
defs: &'a BTreeMap<String, Schema>,
) -> Option<Vec<&'a str>> {
let tags: Vec<&str> = members
.iter()
.map(|m| type_const(m, defs))
.collect::<Option<Vec<_>>>()?;
let unique = tags.iter().collect::<BTreeSet<_>>();
(unique.len() == tags.len()).then_some(tags)
}
fn emit_union(
source: &mut String,
ident: &str,
doc_name: &str,
members: &[RustType],
tags: Option<&[&str]>,
) {
if tags.is_some() {
let _ = writeln!(
source,
"/// One of the alternatives the specification allows for `{doc_name}`.\n\
///\n\
/// Dispatched on the `type` member. Every alternative fixes\n\
/// its own `type`, so the JSON *does* say which one was sent\n\
/// even though it is not wrapped \u{2014} and matching on member order\n\
/// instead would be guessing. `ImageContent` and `AudioContent`\n\
/// are both `{{ data, mimeType }}`; tried in order, an audio\n\
/// block matches the image arm and is re-emitted with\n\
/// `\"type\": \"image\"`, silently and without an error.\n\
///\n\
/// An unrecognised or absent `type` is a decode failure rather\n\
/// than a fall-through, so a malformed message cannot be quietly\n\
/// accepted as whichever alternative happens to parse."
);
let _ = writeln!(source, "#[derive(Debug, Clone, PartialEq, Serialize)]");
} else {
let _ = writeln!(
source,
"/// One of the alternatives the specification allows for `{doc_name}`.\n\
///\n\
/// Emitted untagged: the alternatives are not told apart by a\n\
/// `type` member, so serde matches the first that round-trips and\n\
/// the ordering here is the schema's own."
);
let _ = writeln!(
source,
"#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]"
);
}
let _ = writeln!(source, "#[serde(untagged)]");
let _ = writeln!(source, "pub enum {ident} {{");
for (i, member) in members.iter().enumerate() {
let _ = writeln!(source, " /// Alternative {i}.", i = i + 1);
let _ = writeln!(source, " Member{i}({ty}),", ty = member.render());
}
let _ = writeln!(source, "}}\n");
let Some(tags) = tags else {
return;
};
let _ = writeln!(source, "impl<'de> serde::Deserialize<'de> for {ident} {{");
let _ = writeln!(
source,
" fn deserialize<D>(deserializer: D) -> ::core::result::Result<Self, D::Error>\n \
where\n D: serde::Deserializer<'de>,\n {{"
);
let _ = writeln!(
source,
" let value = serde_json::Value::deserialize(deserializer)?;\n\
\x20 let tag = value\n .get(\"type\")\n .and_then(serde_json::Value::as_str)\n .ok_or_else(|| serde::de::Error::custom(\"a `{doc_name}` must carry a `type` string\"))?;\n\
\x20 match tag {{"
);
for (i, tag) in tags.iter().enumerate() {
let _ = writeln!(
source,
" \"{tag}\" => serde_json::from_value(value)\n \
.map({ident}::Member{i})\n \
.map_err(<D::Error as serde::de::Error>::custom),"
);
}
let _ = writeln!(
source,
" other => Err(<D::Error as serde::de::Error>::custom(format!(\n \
\"`{{other}}` is not a {doc_name}; expected one of {listed}\"))),",
listed = tags.join(", ")
);
let _ = writeln!(source, " }}");
let _ = writeln!(source, " }}");
let _ = writeln!(source, "}}\n");
}
fn emit_def(out: &mut String, name: &str, schema: &Schema, defs: &BTreeMap<String, Schema>) {
let ident = type_ident(name);
if let Some(values) = &schema.enum_values {
let _ = writeln!(
out,
"/// `{name}` — a closed set of string values.\n\
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]\n\
pub enum {ident} {{"
);
for (i, value) in values.iter().enumerate() {
if let Some(s) = value.as_str() {
let _ = writeln!(out, " /// `{s}`.");
let _ = writeln!(out, " #[serde(rename = \"{s}\")]");
let _ = writeln!(out, " {variant},", variant = variant_ident(s, i));
}
}
let _ = writeln!(out, "}}\n");
return;
}
if !schema.is_structured_object() && schema.intersect.is_empty() {
let ty = lower(schema, defs, name);
let _ = ty;
let _ = writeln!(out, "/// `{name}` — see the specification for its shape.");
let _ = writeln!(out, "pub type {ident} = {};\n", ty.render());
return;
}
let mut properties: BTreeMap<&str, &Schema> = BTreeMap::new();
for part in &schema.intersect {
for (name, prop) in &part.properties {
properties.insert(name.as_str(), prop);
}
}
for (name, prop) in &schema.properties {
properties.insert(name.as_str(), prop);
}
let required: Vec<&str> = schema.required.iter().map(String::as_str).collect();
if let Some(doc) = doc_lines(name, schema.description.as_deref(), schema.title.as_deref()) {
let _ = write!(out, "{doc}");
}
let all_default = properties.values().all(|p| {
let ty = lower(p, defs, name);
matches!(ty, RustType::Bool | RustType::Int | RustType::Float | RustType::Str | RustType::Json)
|| matches!(&ty, RustType::List(inner) if matches!(**inner, RustType::Bool | RustType::Int | RustType::Float | RustType::Str | RustType::Json))
});
let derives = if all_default {
"#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]"
} else {
"#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]"
};
let _ = writeln!(out, "{derives}");
let _ = writeln!(out, "pub struct {ident} {{");
for (prop_name, prop) in &properties {
if let Some(doc) = doc_lines(
prop_name,
prop.description.as_deref(),
prop.title.as_deref(),
) {
let _ = write!(out, "{doc}");
}
let ty = lower(prop, defs, &format!("{name}_{prop_name}"));
let field = field_ident(prop_name);
if field != *prop_name {
let _ = writeln!(out, " #[serde(rename = \"{prop_name}\")]");
}
if required.contains(prop_name) {
let _ = writeln!(out, " /// Required by the specification.");
let _ = writeln!(out, " pub {field}: {ty},", ty = ty.render());
} else {
let _ = writeln!(out, " /// Optional in the specification.");
let _ = writeln!(
out,
" #[serde(default, skip_serializing_if = \"Option::is_none\")]"
);
let _ = writeln!(out, " pub {field}: Option<{ty}>,", ty = ty.render());
}
}
let _ = writeln!(
out,
" /// JSON members this type does not name.\n ///\n /// Preserved verbatim so a bridge can forward a field the peer sent that\n /// this revision does not define. Empty rather than absent, and the two\n /// serialise identically.\n #[serde(flatten)]\n pub extra: serde_json::Map<String, serde_json::Value>,"
);
let _ = writeln!(out, "}}\n");
}
fn lower(schema: &Schema, defs: &BTreeMap<String, Schema>, anchor: &str) -> RustType {
if let Some(target) = schema.def_ref() {
if defs.contains_key(target) {
return RustType::Named(type_ident(target));
}
return RustType::Json;
}
if let Some(items) = &schema.items {
return RustType::List(Box::new(lower(items, defs, anchor)));
}
if schema.union.is_some() {
return RustType::Named(union_ident(anchor));
}
if let Some(value) = &schema.constant {
return match value {
Value::Bool(_) => RustType::Bool,
Value::String(_) => RustType::Str,
_ => RustType::Json,
};
}
if schema.enum_values.is_some() {
return RustType::Json;
}
if !schema.intersect.is_empty() {
return schema
.intersect
.iter()
.find_map(|part| part.def_ref())
.map_or(RustType::Json, |r| RustType::Named(type_ident(r)));
}
if schema.type_.is_none() {
return RustType::Json;
}
match &schema.type_ {
Some(TypeSet::Many(names)) => {
RustType::Named(multi_type_ident(names))
}
Some(TypeSet::One(name)) => scalar_named(name),
None => RustType::Json,
}
}
fn multi_type_ident(names: &[String]) -> String {
let mut out = String::from("MultiType");
for name in names {
out.extend(name.chars().map(|c| c.to_ascii_uppercase()));
}
out
}
fn scalar_named(name: &str) -> RustType {
match name {
"boolean" => RustType::Bool,
"integer" => RustType::Int,
"number" => RustType::Float,
"string" => RustType::Str,
_ => RustType::Json,
}
}
fn collect_unions(
schema: &Schema,
owner: &str,
path: &str,
out: &mut Vec<(String, Union, String)>,
) {
if let Some(items) = &schema.items {
collect_unions(items, owner, path, out);
}
if let Some(union) = &schema.union {
out.push((union_ident(path), union.clone(), owner.to_owned()));
for (i, member) in union.members.iter().enumerate() {
collect_unions(member, owner, &format!("{path}_m{i}"), out);
}
return;
}
for (prop_name, prop) in &schema.properties {
collect_unions(prop, owner, &format!("{path}_{prop_name}"), out);
}
for (i, part) in schema.intersect.iter().enumerate() {
collect_unions(part, owner, &format!("{path}_all{i}"), out);
}
}
fn collect_multi_types(schema: &Schema, out: &mut Vec<(String, String)>) {
if let Some(TypeSet::Many(names)) = &schema.type_ {
out.push((multi_type_ident(names), names.join(" or ")));
}
for prop in schema.properties.values() {
collect_multi_types(prop, out);
}
if let Some(items) = &schema.items {
collect_multi_types(items, out);
}
for part in &schema.intersect {
collect_multi_types(part, out);
}
if let Some(union) = &schema.union {
for member in &union.members {
collect_multi_types(member, out);
}
}
}
fn union_ident(path: &str) -> String {
let mut out = String::with_capacity(path.len() + 6);
for segment in path.split('_').filter(|s| !s.is_empty()) {
let mut chars = segment.chars();
if let Some(first) = chars.next() {
out.extend(first.to_uppercase());
out.push_str(chars.as_str());
}
}
format!("{out}Choice")
}
fn member_path(ident: &str, i: usize) -> String {
let base = ident.strip_suffix("Choice").unwrap_or(ident);
format!("{base}_m{i}")
}
fn collect_dangling(schema: &Schema, defs: &BTreeMap<String, Schema>) -> Option<Vec<String>> {
if let Some(target) = schema.def_ref() {
if !defs.contains_key(target) {
return Some(vec![target.to_owned()]);
}
return None;
}
let mut out = Vec::new();
for prop in schema.properties.values() {
out.extend(collect_dangling(prop, defs).unwrap_or_default());
}
if let Some(items) = &schema.items {
out.extend(collect_dangling(items, defs).unwrap_or_default());
}
if let Some(Union { members, .. }) = &schema.union {
for member in members {
out.extend(collect_dangling(member, defs).unwrap_or_default());
}
}
for part in &schema.intersect {
out.extend(collect_dangling(part, defs).unwrap_or_default());
}
(!out.is_empty()).then_some(out)
}
fn doc_lines(name: &str, description: Option<&str>, title: Option<&str>) -> Option<String> {
let mut out = String::new();
let _ = writeln!(out, "/// `{name}`.");
if let Some(title) = title {
let _ = writeln!(out, "///");
let _ = writeln!(out, "/// {title}");
}
if let Some(description) = description {
let _ = writeln!(out, "///");
for line in description.lines() {
let _ = writeln!(out, "/// {line}");
}
}
(!out.is_empty()).then_some(out)
}
pub fn module_ident(revision: &str) -> String {
if revision == "draft" {
return "draft".to_owned();
}
format!("v{}", revision.replace('-', "_"))
}
pub fn type_ident(name: &str) -> String {
let mut out = String::with_capacity(name.len());
for (i, ch) in name.chars().enumerate() {
if ch.is_ascii_alphanumeric() || ch == '_' {
if i == 0 && ch.is_ascii_digit() {
out.push('_');
}
out.push(ch);
} else {
out.push('_');
}
}
if is_keyword(&out) {
out.push('_');
}
out
}
pub fn field_ident(name: &str) -> String {
let snake = to_snake(name);
if RESERVED.iter().any(|r| *r == snake) {
format!("r#{snake}")
} else if is_keyword(&snake) {
format!("{snake}_")
} else if snake.starts_with('_') {
snake.trim_start_matches('_').to_owned()
} else {
snake
}
}
fn to_snake(name: &str) -> String {
let mut out = String::with_capacity(name.len() + 4);
for (i, ch) in name.chars().enumerate() {
if ch.is_ascii_uppercase() {
if i > 0 {
out.push('_');
}
out.push(ch.to_ascii_lowercase());
} else if ch.is_ascii_alphanumeric() {
out.push(ch);
} else {
out.push('_');
}
}
out
}
fn variant_ident(value: &str, index: usize) -> String {
let upper: String = value
.chars()
.filter(char::is_ascii_alphanumeric)
.map(|c| c.to_ascii_uppercase())
.collect();
if upper.is_empty() {
format!("Member{index}")
} else {
upper
}
}
const RESERVED: &[&str] = &[
"abstract", "as", "async", "await", "become", "box", "break", "const", "continue", "crate",
"do", "dyn", "else", "enum", "extern", "false", "final", "fn", "for", "if", "impl", "in",
"let", "loop", "macro", "match", "mod", "move", "mut", "override", "priv", "pub", "ref",
"return", "self", "Self", "static", "struct", "super", "trait", "true", "try", "type",
"typeof", "union", "unsafe", "unsized", "use", "virtual", "where", "while", "yield",
];
fn is_keyword(name: &str) -> bool {
RESERVED.contains(&name)
}