use std::fmt;
use serde_json::{Map, Value};
const DATA_KEYWORDS: [&str; 6] = [
"const",
"default",
"discriminator",
"enum",
"example",
"examples",
];
const CHECKED_PARAMETER_LOCATIONS: [&str; 2] = ["path", "query"];
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum NameKind {
Property,
Parameter,
}
impl fmt::Display for NameKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Property => "property",
Self::Parameter => "parameter",
})
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct NamingViolation {
pub pointer: String,
pub name: String,
pub kind: NameKind,
}
impl fmt::Display for NamingViolation {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"{} {:?} at {}",
self.kind, self.name, self.pointer
)
}
}
#[must_use]
pub fn is_camel_case(name: &str) -> bool {
let mut bytes = name.bytes();
bytes.next().is_some_and(|first| first.is_ascii_lowercase())
&& bytes.all(|byte| byte.is_ascii_alphanumeric())
}
#[must_use]
pub fn find_naming_violations(document: &Value, exemptions: &[&str]) -> Vec<NamingViolation> {
let mut walker = Walker {
exemptions,
violations: Vec::new(),
};
walker.walk(document, &mut String::new());
walker.violations
}
struct Walker<'a> {
exemptions: &'a [&'a str],
violations: Vec<NamingViolation>,
}
impl Walker<'_> {
fn walk(&mut self, value: &Value, pointer: &mut String) {
match value {
Value::Object(object) => self.walk_object(object, pointer),
Value::Array(items) => {
for (index, item) in items.iter().enumerate() {
with_segment(pointer, &index.to_string(), |pointer| {
self.walk(item, pointer)
});
}
}
_ => {}
}
}
fn walk_object(&mut self, object: &Map<String, Value>, pointer: &mut String) {
self.check_parameter(object, pointer);
for (key, child) in object {
if is_skipped_keyword(key) {
continue;
}
with_segment(pointer, key, |pointer| match (key.as_str(), child) {
("properties", Value::Object(properties)) => {
self.walk_properties(properties, pointer)
}
_ => self.walk(child, pointer),
});
}
}
fn walk_properties(&mut self, properties: &Map<String, Value>, pointer: &mut String) {
for (name, schema) in properties {
with_segment(pointer, name, |pointer| {
self.check_name(name, pointer, NameKind::Property);
self.walk(schema, pointer);
});
}
}
fn check_parameter(&mut self, object: &Map<String, Value>, pointer: &str) {
let (Some(Value::String(location)), Some(Value::String(name))) =
(object.get("in"), object.get("name"))
else {
return;
};
if CHECKED_PARAMETER_LOCATIONS.contains(&location.as_str()) {
self.check_name(name, pointer, NameKind::Parameter);
}
}
fn check_name(&mut self, name: &str, pointer: &str, kind: NameKind) {
if is_camel_case(name) || self.exemptions.contains(&name) {
return;
}
self.violations.push(NamingViolation {
pointer: pointer.to_owned(),
name: name.to_owned(),
kind,
});
}
}
fn is_skipped_keyword(key: &str) -> bool {
key.starts_with("x-") || DATA_KEYWORDS.contains(&key)
}
fn with_segment(pointer: &mut String, segment: &str, visit: impl FnOnce(&mut String)) {
let length = pointer.len();
pointer.push('/');
pointer.push_str(&segment.replace('~', "~0").replace('/', "~1"));
visit(pointer);
pointer.truncate(length);
}