use super::*;
#[must_use]
pub fn config_schema() -> serde_json::Value {
let schema = schemars::generate::SchemaSettings::draft07()
.into_generator()
.into_root_schema_for::<Config>();
let mut value = schema.to_value();
canonicalize_schema(&mut value);
value
}
const SCHEMA_KEYWORD_ORDER: &[&str] = &[
"$id",
"$schema",
"title",
"description",
"default",
"deprecated",
"readOnly",
"writeOnly",
"type",
"format",
"enum",
"const",
"allOf",
"anyOf",
"oneOf",
"not",
"if",
"then",
"else",
"multipleOf",
"maximum",
"exclusiveMaximum",
"minimum",
"exclusiveMinimum",
"maxLength",
"minLength",
"pattern",
"items",
"additionalItems",
"maxItems",
"minItems",
"uniqueItems",
"contains",
"maxProperties",
"minProperties",
"required",
"properties",
"patternProperties",
"additionalProperties",
"propertyNames",
"$ref",
"definitions",
];
const SCHEMA_DEFINITION_MAPS: &[&str] = &["properties", "patternProperties", "definitions"];
fn canonicalize_schema(value: &mut serde_json::Value) {
use serde_json::Value;
match value {
Value::Object(map) => {
if let Some(Value::String(d)) = map.get_mut("description") {
*d = collapse_description(d);
}
if let Some(Value::Array(required)) = map.get_mut("required") {
required.sort_by(|a, b| match (a.as_str(), b.as_str()) {
(Some(x), Some(y)) => x.cmp(y),
_ => std::cmp::Ordering::Equal,
});
}
for (key, child) in map.iter_mut() {
match key.as_str() {
k if SCHEMA_DEFINITION_MAPS.contains(&k) => {
if let Value::Object(entries) = child {
sort_object_by_key(entries);
for sub in entries.values_mut() {
canonicalize_schema(sub);
}
}
}
"default" | "enum" | "const" | "examples" => {}
_ => canonicalize_schema(child),
}
}
reorder_object(map, SCHEMA_KEYWORD_ORDER);
}
Value::Array(items) => {
for item in items {
canonicalize_schema(item);
}
}
_ => {}
}
}
fn reorder_object(map: &mut serde_json::Map<String, serde_json::Value>, order: &[&str]) {
let mut keys: Vec<String> = map.keys().cloned().collect();
keys.sort_by(|a, b| {
let rank = |k: &str| order.iter().position(|o| *o == k).unwrap_or(order.len());
rank(a).cmp(&rank(b)).then_with(|| a.cmp(b))
});
let mut rebuilt = serde_json::Map::with_capacity(map.len());
for k in keys {
if let Some(v) = map.remove(&k) {
rebuilt.insert(k, v);
}
}
*map = rebuilt;
}
fn sort_object_by_key(map: &mut serde_json::Map<String, serde_json::Value>) {
let mut keys: Vec<String> = map.keys().cloned().collect();
keys.sort();
let mut rebuilt = serde_json::Map::with_capacity(map.len());
for k in keys {
if let Some(v) = map.remove(&k) {
rebuilt.insert(k, v);
}
}
*map = rebuilt;
}
#[must_use]
pub fn collapse_description(s: &str) -> String {
let mut segments: Vec<String> = Vec::new();
let mut prose: Vec<&str> = Vec::new();
let mut fenced: Vec<&str> = Vec::new();
let mut in_fence = false;
let push_prose = |prose: &mut Vec<&str>, segments: &mut Vec<String>| {
if prose.is_empty() {
return;
}
let text = collapse_prose(&prose.join("\n"));
prose.clear();
if !text.is_empty() {
segments.push(text);
}
};
for line in s.split('\n') {
if line.trim_start().starts_with("```") {
if in_fence {
fenced.push(line);
segments.push(fenced.join("\n"));
fenced.clear();
} else {
push_prose(&mut prose, &mut segments);
fenced.push(line);
}
in_fence = !in_fence;
continue;
}
if in_fence {
fenced.push(line);
} else {
prose.push(line);
}
}
prose.append(&mut fenced);
push_prose(&mut prose, &mut segments);
segments.join("\n\n")
}
fn collapse_prose(s: &str) -> String {
s.split("\n\n")
.map(|para| {
para.split('\n')
.map(str::trim)
.collect::<Vec<_>>()
.join(" ")
.trim()
.to_string()
})
.filter(|para| !para.is_empty())
.collect::<Vec<_>>()
.join("\n\n")
}
#[cfg(test)]
mod tests {
use super::collapse_description;
#[test]
fn collapse_description_joins_hard_wrapped_lines_and_keeps_paragraphs() {
assert_eq!(
collapse_description("a doc comment\nwrapped by rustfmt\n\nsecond paragraph"),
"a doc comment wrapped by rustfmt\n\nsecond paragraph"
);
}
#[test]
fn collapse_description_preserves_fenced_block_line_breaks() {
let doc = "YAML examples:\n\n```yaml\nheader: \"inline\"\nheader:\n from_file: ./H.md\n```\n\nBoth are rendered.";
assert_eq!(
collapse_description(doc),
"YAML examples:\n\n```yaml\nheader: \"inline\"\nheader:\n from_file: ./H.md\n```\n\nBoth are rendered."
);
}
#[test]
fn collapse_description_still_collapses_prose_around_a_fence() {
let doc = "lead-in prose\nwrapped\n\n```yaml\na: 1\n```\n\ntrailing prose\nwrapped too";
assert_eq!(
collapse_description(doc),
"lead-in prose wrapped\n\n```yaml\na: 1\n```\n\ntrailing prose wrapped too"
);
}
#[test]
fn collapse_description_keeps_indentation_inside_a_fence() {
let doc = "```yaml\ncompletions:\n generate: \"x\"\n shells: [bash]\n```";
assert_eq!(
collapse_description(doc),
"```yaml\ncompletions:\n generate: \"x\"\n shells: [bash]\n```"
);
}
#[test]
fn collapse_description_treats_an_unterminated_fence_as_prose() {
assert_eq!(
collapse_description("intro\n\n```yaml\na: 1\nb: 2"),
"intro\n\n```yaml a: 1 b: 2"
);
}
}