use std::collections::HashMap;
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TaggedEnumField {
pub path: String,
pub variants: Vec<String>,
}
pub fn schema_to_yaml_template(schema: &Value, indent_spaces: usize) -> String {
schema_to_yaml_template_with_choices(schema, indent_spaces, &HashMap::new())
}
pub fn schema_to_yaml_template_with_choices(
schema: &Value,
indent_spaces: usize,
choices: &HashMap<String, String>,
) -> String {
let defs = schema.get("$defs");
let mut out = String::new();
emit_object_properties(schema, defs, indent_spaces, choices, &mut out);
if out.is_empty() {
let pad = " ".repeat(indent_spaces);
out.push_str(&format!("{pad}{{}}\n"));
}
out
}
pub fn discover_tagged_enum_fields(schema: &Value) -> Vec<TaggedEnumField> {
let defs = schema.get("$defs");
let resolved = resolve_ref_borrowed(schema, defs);
let Some(props) = resolved.get("properties").and_then(|v| v.as_object()) else {
return Vec::new();
};
let mut out = Vec::new();
for (key, prop_schema) in props {
let prop_resolved = resolve_ref_borrowed(prop_schema, defs);
if let Some(variants) = tagged_enum_variants(prop_resolved, defs) {
out.push(TaggedEnumField {
path: key.clone(),
variants: variants.iter().map(|v| v.tag.to_string()).collect(),
});
}
}
out
}
fn emit_object_properties(
schema: &Value,
defs: Option<&Value>,
indent: usize,
choices: &HashMap<String, String>,
out: &mut String,
) {
let schema = resolve_ref(schema, defs);
let Some(props) = schema.get("properties").and_then(|v| v.as_object()) else {
return;
};
let required: Vec<&str> = schema
.get("required")
.and_then(|v| v.as_array())
.map(|a| a.iter().filter_map(|v| v.as_str()).collect())
.unwrap_or_default();
for (key, prop_schema) in props {
let is_required = required.contains(&key.as_str());
emit_property(key, prop_schema, is_required, defs, indent, choices, out);
}
}
#[allow(clippy::too_many_arguments)] fn emit_property(
key: &str,
schema: &Value,
required: bool,
defs: Option<&Value>,
indent: usize,
choices: &HashMap<String, String>,
out: &mut String,
) {
let pad = " ".repeat(indent);
let resolved = resolve_ref(schema, defs);
let description = resolved
.get("description")
.and_then(|v| v.as_str())
.map(collapse_whitespace);
if let Some(variants) = tagged_enum_variants(&resolved, defs) {
emit_tagged_enum(
key,
&variants,
required,
&description,
defs,
indent,
choices,
out,
);
return;
}
if is_object_with_properties(&resolved) {
if required {
out.push_str(&format!("{pad}{key}:\n"));
emit_object_properties(&resolved, defs, indent + 2, choices, out);
} else {
let line_comment = describe(&description, &resolved);
let suffix = if line_comment.is_empty() {
String::new()
} else {
format!(" # {line_comment}")
};
out.push_str(&format!("{pad}# {key}: {{ ... }}{suffix}\n"));
}
return;
}
let placeholder = type_placeholder(&resolved);
let value = resolved
.get("default")
.map(render_default)
.unwrap_or(placeholder);
let mut comment_parts: Vec<String> = Vec::new();
if required {
comment_parts.push("REQUIRED".to_string());
}
if let Some(d) = description.as_ref()
&& !d.is_empty()
{
comment_parts.push(d.clone());
}
if let Some(values) = enum_string_values(&resolved) {
comment_parts.push(format!("one of: {}", values.join(", ")));
}
let comment = if comment_parts.is_empty() {
String::new()
} else {
format!(" # {}", comment_parts.join(" — "))
};
if required {
out.push_str(&format!("{pad}{key}: {value}{comment}\n"));
} else {
out.push_str(&format!("{pad}# {key}: {value}{comment}\n"));
}
}
#[allow(clippy::too_many_arguments)] fn emit_tagged_enum(
key: &str,
variants: &[TaggedVariant<'_>],
required: bool,
_description: &Option<String>,
defs: Option<&Value>,
indent: usize,
choices: &HashMap<String, String>,
out: &mut String,
) {
let pad = " ".repeat(indent);
let inner_pad = " ".repeat(indent + 2);
let all_tags: Vec<&str> = variants.iter().map(|v| v.tag).collect();
let chosen_idx = choices
.get(key)
.and_then(|tag| variants.iter().position(|v| v.tag == tag))
.unwrap_or(0);
let chosen = &variants[chosen_idx];
if required {
out.push_str(&format!("{pad}{key}:\n"));
out.push_str(&format!(
"{inner_pad}type: {tag} # one of: {tags}\n",
tag = chosen.tag,
tags = all_tags.join(", "),
));
for (field_key, field_schema, field_required) in &chosen.fields {
if *field_key == chosen.discriminator {
continue;
}
emit_property(
field_key,
field_schema,
*field_required,
defs,
indent + 2,
choices,
out,
);
}
emit_alternative_variants(variants, chosen_idx, defs, indent + 2, out);
} else {
out.push_str(&format!(
"{pad}# {key}: {{ type: {tag} }} # one of: {tags}\n",
tag = chosen.tag,
tags = all_tags.join(", "),
));
emit_alternative_variants(variants, chosen_idx, defs, indent + 2, out);
}
}
fn emit_alternative_variants(
variants: &[TaggedVariant<'_>],
chosen_idx: usize,
defs: Option<&Value>,
indent: usize,
out: &mut String,
) {
let alternatives: Vec<&TaggedVariant<'_>> = variants
.iter()
.enumerate()
.filter_map(|(i, v)| if i == chosen_idx { None } else { Some(v) })
.collect();
if alternatives.is_empty() {
return;
}
let pad = " ".repeat(indent);
out.push_str(&format!(
"{pad}# --- Alternative variants — replace the block above with one of these ---\n"
));
for (i, alt) in alternatives.iter().enumerate() {
if i > 0 {
out.push_str(&format!("{pad}#\n"));
}
out.push_str(&format!("{pad}# type: {tag}\n", tag = alt.tag));
for (field_key, field_schema, field_required) in &alt.fields {
if *field_key == alt.discriminator {
continue;
}
let resolved = resolve_ref(field_schema, defs);
if is_object_with_properties(&resolved) {
out.push_str(&format!("{pad}# {field_key}:\n"));
emit_commented_object_props(&resolved, defs, indent + 2, out);
} else {
let placeholder = resolved
.get("default")
.map(render_default)
.unwrap_or_else(|| type_placeholder(&resolved));
let marker = if *field_required {
" # REQUIRED"
} else {
""
};
out.push_str(&format!("{pad}# {field_key}: {placeholder}{marker}\n"));
}
}
}
}
fn emit_commented_object_props(
schema: &Value,
defs: Option<&Value>,
indent: usize,
out: &mut String,
) {
let pad = " ".repeat(indent);
let Some(props) = schema.get("properties").and_then(|v| v.as_object()) else {
return;
};
let required: Vec<&str> = schema
.get("required")
.and_then(|v| v.as_array())
.map(|a| a.iter().filter_map(|v| v.as_str()).collect())
.unwrap_or_default();
for (k, s) in props {
let resolved = resolve_ref(s, defs);
let req = required.contains(&k.as_str());
if is_object_with_properties(&resolved) {
out.push_str(&format!("{pad}# {k}:\n"));
emit_commented_object_props(&resolved, defs, indent + 2, out);
} else {
let placeholder = resolved
.get("default")
.map(render_default)
.unwrap_or_else(|| type_placeholder(&resolved));
let marker = if req { " # REQUIRED" } else { "" };
out.push_str(&format!("{pad}# {k}: {placeholder}{marker}\n"));
}
}
}
struct TaggedVariant<'a> {
tag: &'a str,
discriminator: &'a str,
fields: Vec<(&'a str, &'a Value, bool)>,
}
fn tagged_enum_variants<'a>(
schema: &'a Value,
defs: Option<&'a Value>,
) -> Option<Vec<TaggedVariant<'a>>> {
let arr = match schema.get("oneOf").and_then(|v| v.as_array()) {
Some(a) => a,
None => {
let any = schema.get("anyOf")?.as_array()?;
let inner = any
.iter()
.map(|m| resolve_ref_borrowed(m, defs))
.find(|r| r.get("oneOf").is_some())?;
inner.get("oneOf")?.as_array()?
}
};
if arr.is_empty() {
return None;
}
let first = resolve_ref_borrowed(&arr[0], defs);
let props = first.get("properties")?.as_object()?;
let (disc, _) = props
.iter()
.find(|(_, v)| v.get("const").and_then(|c| c.as_str()).is_some())?;
let mut variants = Vec::new();
for v in arr {
let resolved = resolve_ref_borrowed(v, defs);
let v_props = resolved.get("properties").and_then(|p| p.as_object())?;
let tag = v_props
.get(disc)
.and_then(|t| t.get("const"))
.and_then(|c| c.as_str())?;
let required: Vec<&str> = resolved
.get("required")
.and_then(|r| r.as_array())
.map(|a| a.iter().filter_map(|v| v.as_str()).collect())
.unwrap_or_default();
let fields = v_props
.iter()
.map(|(k, s)| (k.as_str(), s, required.contains(&k.as_str())))
.collect();
variants.push(TaggedVariant {
tag,
discriminator: disc,
fields,
});
}
Some(variants)
}
fn is_object_with_properties(schema: &Value) -> bool {
schema_type(schema) == Some("object") && schema.get("properties").is_some()
}
fn schema_type(schema: &Value) -> Option<&str> {
match schema.get("type") {
Some(Value::String(s)) => Some(s.as_str()),
Some(Value::Array(arr)) => arr.iter().filter_map(|v| v.as_str()).find(|s| *s != "null"),
_ => None,
}
}
fn type_placeholder(schema: &Value) -> String {
match schema_type(schema) {
Some("string") => "\"\"".to_string(),
Some("integer") | Some("number") => "0".to_string(),
Some("boolean") => "false".to_string(),
Some("array") => "[]".to_string(),
Some("object") => "{}".to_string(),
_ => "null".to_string(),
}
}
fn render_default(v: &Value) -> String {
match v {
Value::Null => "null".to_string(),
Value::Bool(b) => b.to_string(),
Value::Number(n) => n.to_string(),
Value::String(s) => format!("\"{}\"", s.replace('"', "\\\"")),
Value::Array(a) if a.is_empty() => "[]".to_string(),
Value::Object(o) if o.is_empty() => "{}".to_string(),
other => other.to_string(),
}
}
fn enum_string_values(schema: &Value) -> Option<Vec<&str>> {
let arr = schema.get("enum")?.as_array()?;
let values: Vec<&str> = arr.iter().filter_map(|v| v.as_str()).collect();
if values.is_empty() {
None
} else {
Some(values)
}
}
fn describe(description: &Option<String>, schema: &Value) -> String {
let mut parts: Vec<String> = Vec::new();
if let Some(d) = description.as_ref()
&& !d.is_empty()
{
parts.push(d.clone());
}
if let Some(values) = enum_string_values(schema) {
parts.push(format!("one of: {}", values.join(", ")));
}
parts.join(" — ")
}
fn collapse_whitespace(s: &str) -> String {
let collapsed: String = s.split_whitespace().collect::<Vec<_>>().join(" ");
const MAX: usize = 120;
if let Some(idx) = collapsed.find(". ") {
let head = &collapsed[..idx + 1];
return head.to_string();
}
if collapsed.chars().count() > MAX {
let mut truncated: String = collapsed.chars().take(MAX).collect();
truncated.push('…');
return truncated;
}
collapsed
}
fn resolve_ref(schema: &Value, defs: Option<&Value>) -> Value {
resolve_ref_borrowed(schema, defs).clone()
}
fn resolve_ref_borrowed<'a>(schema: &'a Value, defs: Option<&'a Value>) -> &'a Value {
let Some(reference) = schema.get("$ref").and_then(|v| v.as_str()) else {
return schema;
};
let Some(name) = reference.strip_prefix("#/$defs/") else {
return schema;
};
defs.and_then(|d| d.get(name)).unwrap_or(schema)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn empty_object_schema_emits_brace_placeholder() {
let schema = json!({ "type": "object" });
let yaml = schema_to_yaml_template(&schema, 6);
assert_eq!(yaml, " {}\n");
}
#[test]
fn required_string_field_gets_quoted_placeholder_and_required_marker() {
let schema = json!({
"type": "object",
"properties": {
"path": { "type": "string", "description": "Path to the output file." }
},
"required": ["path"]
});
let yaml = schema_to_yaml_template(&schema, 6);
assert!(yaml.contains("path: \"\""), "missing path key: {yaml}");
assert!(
yaml.contains("# REQUIRED"),
"missing REQUIRED comment: {yaml}"
);
assert!(yaml.contains("Path to the output file."));
}
#[test]
fn required_integer_field_gets_zero_placeholder() {
let schema = json!({
"type": "object",
"properties": { "port": { "type": "integer" } },
"required": ["port"]
});
let yaml = schema_to_yaml_template(&schema, 0);
assert!(yaml.contains("port: 0"));
assert!(yaml.contains("# REQUIRED"));
}
#[test]
fn required_boolean_field_gets_false_placeholder() {
let schema = json!({
"type": "object",
"properties": { "ssl": { "type": "boolean" } },
"required": ["ssl"]
});
let yaml = schema_to_yaml_template(&schema, 0);
assert!(yaml.contains("ssl: false"));
}
#[test]
fn optional_field_with_default_is_commented_out_with_default_value() {
let schema = json!({
"type": "object",
"properties": {
"batch_size": { "type": "integer", "default": 1000, "description": "Batch size." }
}
});
let yaml = schema_to_yaml_template(&schema, 0);
assert!(yaml.contains("# batch_size: 1000"));
assert!(yaml.contains("Batch size."));
}
#[test]
fn optional_field_without_default_is_commented_out_with_placeholder() {
let schema = json!({
"type": "object",
"properties": {
"label": { "type": "string", "description": "Friendly label." }
}
});
let yaml = schema_to_yaml_template(&schema, 0);
assert!(yaml.contains("# label: \"\""));
assert!(yaml.contains("Friendly label."));
}
#[test]
fn enum_values_appear_in_comment() {
let schema = json!({
"type": "object",
"properties": {
"method": {
"type": "string",
"enum": ["GET", "POST", "PUT", "PATCH", "DELETE"],
"default": "GET",
"description": "HTTP method."
}
}
});
let yaml = schema_to_yaml_template(&schema, 0);
assert!(yaml.contains("# method: \"GET\""), "yaml: {yaml}");
assert!(
yaml.contains("one of: GET, POST, PUT, PATCH, DELETE"),
"yaml: {yaml}"
);
}
#[test]
fn required_nested_object_recurses() {
let schema = json!({
"type": "object",
"properties": {
"address": {
"type": "object",
"properties": {
"city": { "type": "string" }
},
"required": ["city"]
}
},
"required": ["address"]
});
let yaml = schema_to_yaml_template(&schema, 0);
assert!(yaml.contains("address:\n"), "yaml: {yaml}");
assert!(yaml.contains(" city: \"\""), "yaml: {yaml}");
assert!(yaml.contains("# REQUIRED"));
}
#[test]
fn optional_nested_object_flattens_to_comment() {
let schema = json!({
"type": "object",
"properties": {
"tls": {
"type": "object",
"properties": { "ca_path": { "type": "string" } },
"description": "TLS settings."
}
}
});
let yaml = schema_to_yaml_template(&schema, 0);
assert!(yaml.contains("# tls: { ... }"), "yaml: {yaml}");
assert!(yaml.contains("TLS settings."));
}
#[test]
fn tagged_enum_required_expands_first_variant_inline() {
let schema = json!({
"type": "object",
"properties": {
"auth": {
"oneOf": [
{
"type": "object",
"properties": { "type": { "const": "none" } },
"required": ["type"]
},
{
"type": "object",
"properties": {
"type": { "const": "bearer" },
"token": { "type": "string" }
},
"required": ["type", "token"]
}
]
}
},
"required": ["auth"]
});
let yaml = schema_to_yaml_template(&schema, 0);
assert!(yaml.contains("auth:\n"), "yaml: {yaml}");
assert!(yaml.contains("type: none"), "yaml: {yaml}");
assert!(yaml.contains("one of: none, bearer"), "yaml: {yaml}");
}
#[test]
fn tagged_enum_optional_flattens_to_first_variant_comment() {
let schema = json!({
"type": "object",
"properties": {
"auth": {
"oneOf": [
{
"type": "object",
"properties": { "type": { "const": "none" } },
"required": ["type"]
},
{
"type": "object",
"properties": {
"type": { "const": "bearer" },
"token": { "type": "string" }
},
"required": ["type", "token"]
}
]
}
}
});
let yaml = schema_to_yaml_template(&schema, 0);
assert!(yaml.contains("# auth: { type: none }"), "yaml: {yaml}");
assert!(yaml.contains("one of: none, bearer"), "yaml: {yaml}");
}
#[test]
fn adjacent_tagged_enum_nests_config_block() {
let schema = json!({
"type": "object",
"properties": {
"auth": {
"oneOf": [
{
"type": "object",
"properties": { "type": { "const": "none" } },
"required": ["type"]
},
{
"type": "object",
"properties": {
"type": { "const": "bearer" },
"config": {
"type": "object",
"properties": { "token": { "type": "string" } },
"required": ["token"]
}
},
"required": ["type", "config"]
}
]
}
},
"required": ["auth"]
});
let yaml = schema_to_yaml_template(&schema, 0);
assert!(yaml.contains("type: none"), "yaml: {yaml}");
assert!(yaml.contains("# type: bearer"), "yaml: {yaml}");
assert!(yaml.contains("# config:"), "yaml: {yaml}");
assert!(yaml.contains("# token: \"\""), "yaml: {yaml}");
}
#[test]
fn ref_to_defs_is_resolved() {
let schema = json!({
"type": "object",
"properties": {
"creds": { "$ref": "#/$defs/Creds" }
},
"required": ["creds"],
"$defs": {
"Creds": {
"type": "object",
"properties": { "token": { "type": "string" } },
"required": ["token"]
}
}
});
let yaml = schema_to_yaml_template(&schema, 0);
assert!(yaml.contains("creds:\n"), "yaml: {yaml}");
assert!(yaml.contains(" token: \"\""), "yaml: {yaml}");
}
#[test]
fn tagged_enum_required_lists_other_variants_as_commented_alternatives() {
let schema = json!({
"type": "object",
"properties": {
"auth": {
"oneOf": [
{
"type": "object",
"properties": { "type": { "const": "none" } },
"required": ["type"]
},
{
"type": "object",
"properties": {
"type": { "const": "bearer" },
"token": { "type": "string" }
},
"required": ["type", "token"]
},
{
"type": "object",
"properties": {
"type": { "const": "basic" },
"username": { "type": "string" },
"password": { "type": "string" }
},
"required": ["type", "username", "password"]
}
]
}
},
"required": ["auth"]
});
let yaml = schema_to_yaml_template(&schema, 0);
assert!(yaml.contains("type: none"), "yaml: {yaml}");
assert!(yaml.contains("type: bearer"), "yaml: {yaml}");
assert!(yaml.contains("type: basic"), "yaml: {yaml}");
assert!(yaml.contains("# token: \"\""), "yaml: {yaml}");
assert!(yaml.contains("# username: \"\""), "yaml: {yaml}");
assert!(yaml.contains("# password: \"\""), "yaml: {yaml}");
}
#[test]
fn tagged_enum_with_explicit_choice_inlines_that_variant() {
use std::collections::HashMap;
let schema = json!({
"type": "object",
"properties": {
"auth": {
"oneOf": [
{
"type": "object",
"properties": { "type": { "const": "none" } },
"required": ["type"]
},
{
"type": "object",
"properties": {
"type": { "const": "bearer" },
"token": { "type": "string" }
},
"required": ["type", "token"]
}
]
}
},
"required": ["auth"]
});
let mut choices = HashMap::new();
choices.insert("auth".to_string(), "bearer".to_string());
let yaml = schema_to_yaml_template_with_choices(&schema, 0, &choices);
let token_line = yaml
.lines()
.find(|l| l.contains("token:"))
.expect("token line missing");
assert!(
!token_line.trim_start().starts_with('#'),
"expected chosen variant's token to be uncommented; got: {token_line:?}"
);
assert!(
token_line.contains("REQUIRED"),
"chosen variant fields should carry REQUIRED marker; got: {token_line:?}"
);
let none_line = yaml
.lines()
.find(|l| l.contains("type: none"))
.expect("none variant missing from alternatives");
assert!(
none_line.trim_start().starts_with('#'),
"non-chosen variant should be commented out; got: {none_line:?}"
);
}
#[test]
fn discover_tagged_enum_fields_finds_top_level_oneof_properties() {
let schema = json!({
"type": "object",
"properties": {
"auth": {
"oneOf": [
{
"type": "object",
"properties": { "type": { "const": "none" } },
"required": ["type"]
},
{
"type": "object",
"properties": {
"type": { "const": "bearer" },
"token": { "type": "string" }
},
"required": ["type", "token"]
}
]
},
"path": { "type": "string" }
}
});
let fields = discover_tagged_enum_fields(&schema);
assert_eq!(fields.len(), 1, "expected exactly one tagged-enum field");
assert_eq!(fields[0].path, "auth");
assert_eq!(fields[0].variants, vec!["none", "bearer"]);
}
#[test]
fn discover_tagged_enum_fields_resolves_refs() {
let schema = json!({
"type": "object",
"properties": {
"auth": { "$ref": "#/$defs/Auth" }
},
"$defs": {
"Auth": {
"oneOf": [
{
"type": "object",
"properties": { "type": { "const": "none" } },
"required": ["type"]
},
{
"type": "object",
"properties": {
"type": { "const": "bearer" },
"token": { "type": "string" }
},
"required": ["type", "token"]
}
]
}
}
});
let fields = discover_tagged_enum_fields(&schema);
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].path, "auth");
assert_eq!(fields[0].variants, vec!["none", "bearer"]);
}
#[test]
fn output_indent_matches_requested_column() {
let schema = json!({
"type": "object",
"properties": { "name": { "type": "string" } },
"required": ["name"]
});
let yaml = schema_to_yaml_template(&schema, 4);
for line in yaml.lines() {
if !line.trim().is_empty() {
assert!(
line.starts_with(" "),
"line not indented 4 spaces: {line:?}"
);
}
}
}
}