1use std::fmt;
4
5use serde_json::{Map, Value};
6
7const DATA_KEYWORDS: [&str; 6] = [
9 "const",
10 "default",
11 "discriminator",
12 "enum",
13 "example",
14 "examples",
15];
16const CHECKED_PARAMETER_LOCATIONS: [&str; 2] = ["path", "query"];
18
19#[derive(Clone, Copy, Debug, Eq, PartialEq)]
21pub enum NameKind {
22 Property,
24 Parameter,
26}
27
28impl fmt::Display for NameKind {
29 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
30 formatter.write_str(match self {
31 Self::Property => "property",
32 Self::Parameter => "parameter",
33 })
34 }
35}
36
37#[derive(Clone, Debug, Eq, PartialEq)]
39pub struct NamingViolation {
40 pub pointer: String,
42 pub name: String,
44 pub kind: NameKind,
46}
47
48impl fmt::Display for NamingViolation {
49 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50 write!(
51 formatter,
52 "{} {:?} at {}",
53 self.kind, self.name, self.pointer
54 )
55 }
56}
57
58#[must_use]
61pub fn is_camel_case(name: &str) -> bool {
62 let mut bytes = name.bytes();
63 bytes.next().is_some_and(|first| first.is_ascii_lowercase())
64 && bytes.all(|byte| byte.is_ascii_alphanumeric())
65}
66
67#[must_use]
74pub fn find_naming_violations(document: &Value, exemptions: &[&str]) -> Vec<NamingViolation> {
75 let mut walker = Walker {
76 exemptions,
77 violations: Vec::new(),
78 };
79 walker.walk(document, &mut String::new());
80 walker.violations
81}
82
83struct Walker<'a> {
84 exemptions: &'a [&'a str],
85 violations: Vec<NamingViolation>,
86}
87
88impl Walker<'_> {
89 fn walk(&mut self, value: &Value, pointer: &mut String) {
90 match value {
91 Value::Object(object) => self.walk_object(object, pointer),
92 Value::Array(items) => {
93 for (index, item) in items.iter().enumerate() {
94 with_segment(pointer, &index.to_string(), |pointer| {
95 self.walk(item, pointer)
96 });
97 }
98 }
99 _ => {}
100 }
101 }
102
103 fn walk_object(&mut self, object: &Map<String, Value>, pointer: &mut String) {
104 self.check_parameter(object, pointer);
105 for (key, child) in object {
106 if is_skipped_keyword(key) {
107 continue;
108 }
109 with_segment(pointer, key, |pointer| match (key.as_str(), child) {
110 ("properties", Value::Object(properties)) => {
111 self.walk_properties(properties, pointer)
112 }
113 _ => self.walk(child, pointer),
114 });
115 }
116 }
117
118 fn walk_properties(&mut self, properties: &Map<String, Value>, pointer: &mut String) {
119 for (name, schema) in properties {
120 with_segment(pointer, name, |pointer| {
121 self.check_name(name, pointer, NameKind::Property);
122 self.walk(schema, pointer);
123 });
124 }
125 }
126
127 fn check_parameter(&mut self, object: &Map<String, Value>, pointer: &str) {
128 let (Some(Value::String(location)), Some(Value::String(name))) =
129 (object.get("in"), object.get("name"))
130 else {
131 return;
132 };
133 if CHECKED_PARAMETER_LOCATIONS.contains(&location.as_str()) {
134 self.check_name(name, pointer, NameKind::Parameter);
135 }
136 }
137
138 fn check_name(&mut self, name: &str, pointer: &str, kind: NameKind) {
139 if is_camel_case(name) || self.exemptions.contains(&name) {
140 return;
141 }
142 self.violations.push(NamingViolation {
143 pointer: pointer.to_owned(),
144 name: name.to_owned(),
145 kind,
146 });
147 }
148}
149
150fn is_skipped_keyword(key: &str) -> bool {
151 key.starts_with("x-") || DATA_KEYWORDS.contains(&key)
152}
153
154fn with_segment(pointer: &mut String, segment: &str, visit: impl FnOnce(&mut String)) {
155 let length = pointer.len();
156 pointer.push('/');
157 pointer.push_str(&segment.replace('~', "~0").replace('/', "~1"));
158 visit(pointer);
159 pointer.truncate(length);
160}