data_protocol_validator/
suggestions.rs1use serde_json::Value;
2
3use crate::types::Suggestion;
4
5fn number_to_json_value(n: f64) -> Value {
9 if n.fract() == 0.0 && n.abs() < (i64::MAX as f64) {
10 serde_json::json!(n as i64)
11 } else {
12 serde_json::json!(n)
13 }
14}
15
16pub fn suggest_type_fix(data: &Value, expected_type: &str) -> Option<Suggestion> {
18 match expected_type {
19 "number" | "integer" => {
20 if let Some(s) = data.as_str() {
21 let trimmed = s.trim();
22 if !trimmed.is_empty() {
23 if let Ok(num) = trimmed.parse::<f64>() {
24 if expected_type == "integer" {
25 if num.fract() == 0.0 {
26 return Some(Suggestion {
27 action: "convert".to_string(),
28 description: format!("Convert \"{}\" to integer", s),
29 suggested_value: Some(number_to_json_value(num)),
30 });
31 }
32 return None;
34 }
35 return Some(Suggestion {
36 action: "convert".to_string(),
37 description: format!("Convert \"{}\" to number", s),
38 suggested_value: Some(number_to_json_value(num)),
39 });
40 }
41 }
42 }
43 if let Some(b) = data.as_bool() {
44 let num = if b { 1 } else { 0 };
45 return Some(Suggestion {
46 action: "convert".to_string(),
47 description: format!("Convert {} to number", b),
48 suggested_value: Some(serde_json::json!(num)),
49 });
50 }
51 None
52 }
53 "string" => {
54 if let Some(n) = data.as_f64() {
55 let s = format_number_as_js(n);
57 return Some(Suggestion {
58 action: "convert".to_string(),
59 description: format!("Convert {} to string", format_json_value(data)),
60 suggested_value: Some(Value::String(s)),
61 });
62 }
63 if let Some(b) = data.as_bool() {
64 return Some(Suggestion {
65 action: "convert".to_string(),
66 description: format!("Convert {} to string", b),
67 suggested_value: Some(Value::String(b.to_string())),
68 });
69 }
70 None
71 }
72 "boolean" => {
73 if let Some(s) = data.as_str() {
74 if s == "true" || s == "false" {
75 let bval = s == "true";
76 return Some(Suggestion {
77 action: "convert".to_string(),
78 description: format!("Convert \"{}\" to boolean", s),
79 suggested_value: Some(serde_json::json!(bval)),
80 });
81 }
82 }
83 None
84 }
85 _ => None,
86 }
87}
88
89pub fn suggest_number_fix(data: f64, schema: &Value) -> Option<Suggestion> {
91 if let Some(min) = schema.get("minimum").and_then(|v| v.as_f64()) {
92 if data < min {
93 return Some(Suggestion {
94 action: "clamp".to_string(),
95 description: format!("Clamp value to minimum {}", format_number(min)),
96 suggested_value: Some(number_to_json_value(min)),
97 });
98 }
99 }
100 if let Some(max) = schema.get("maximum").and_then(|v| v.as_f64()) {
101 if data > max {
102 return Some(Suggestion {
103 action: "clamp".to_string(),
104 description: format!("Clamp value to maximum {}", format_number(max)),
105 suggested_value: Some(number_to_json_value(max)),
106 });
107 }
108 }
109 if let Some(exc_min) = schema.get("exclusiveMinimum").and_then(|v| v.as_f64()) {
110 if data <= exc_min {
111 return Some(Suggestion {
112 action: "clamp".to_string(),
113 description: format!("Value must be greater than {}", format_number(exc_min)),
114 suggested_value: Some(number_to_json_value(exc_min)),
115 });
116 }
117 }
118 if let Some(exc_max) = schema.get("exclusiveMaximum").and_then(|v| v.as_f64()) {
119 if data >= exc_max {
120 return Some(Suggestion {
121 action: "clamp".to_string(),
122 description: format!("Value must be less than {}", format_number(exc_max)),
123 suggested_value: Some(number_to_json_value(exc_max)),
124 });
125 }
126 }
127 None
128}
129
130pub fn suggest_string_fix(data: &str, schema: &Value) -> Option<Suggestion> {
132 if let Some(max_len) = schema.get("maxLength").and_then(|v| v.as_u64()) {
133 let max_len = max_len as usize;
134 if data.len() > max_len {
135 let truncated: String = data.chars().take(max_len).collect();
136 return Some(Suggestion {
137 action: "truncate".to_string(),
138 description: format!("Truncate string to {} characters", max_len),
139 suggested_value: Some(Value::String(truncated)),
140 });
141 }
142 }
143 None
144}
145
146pub fn suggest_missing_required(property: &str) -> Suggestion {
148 Suggestion {
149 action: "add".to_string(),
150 description: format!("Add required property \"{}\"", property),
151 suggested_value: None,
152 }
153}
154
155pub fn suggest_remove_additional(property: &str) -> Suggestion {
157 Suggestion {
158 action: "remove".to_string(),
159 description: format!("Remove additional property \"{}\"", property),
160 suggested_value: None,
161 }
162}
163
164pub fn suggest_array_fix(schema: &Value) -> Option<Suggestion> {
166 if let Some(max_items) = schema.get("maxItems").and_then(|v| v.as_u64()) {
167 return Some(Suggestion {
168 action: "truncate".to_string(),
169 description: format!("Truncate array to {} items", max_items),
170 suggested_value: None,
171 });
172 }
173 None
174}
175
176fn format_number_as_js(n: f64) -> String {
178 if n.fract() == 0.0 && n.abs() < 1e15 {
179 format!("{}", n as i64)
180 } else {
181 format!("{}", n)
182 }
183}
184
185fn format_number(n: f64) -> String {
187 if n.fract() == 0.0 && n.abs() < 1e15 {
188 format!("{}", n as i64)
189 } else {
190 format!("{}", n)
191 }
192}
193
194fn format_json_value(v: &Value) -> String {
196 match v {
197 Value::Number(n) => {
198 if let Some(f) = n.as_f64() {
199 format_number(f)
200 } else {
201 n.to_string()
202 }
203 }
204 _ => v.to_string(),
205 }
206}