Skip to main content

data_protocol_validator/
suggestions.rs

1use serde_json::Value;
2
3use crate::types::Suggestion;
4
5/// Convert a f64 to a `serde_json::Value`, using an integer representation
6/// when the value has no fractional part (matching how JavaScript / JSON would
7/// represent it).
8fn 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
16/// Suggest a type conversion fix for a type mismatch error (E001).
17pub 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                            // float string -> integer requested but not an integer
33                            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                // Format like JSON.stringify / String(data) in JS
56                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
89/// Suggest a clamp fix for an out-of-range number (E005).
90pub 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
130/// Suggest a truncation fix for a string that is too long (E004 maxLength).
131pub 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
146/// Suggest adding a missing required property (E002).
147pub 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
155/// Suggest removing an additional property (E003).
156pub 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
164/// Suggest truncating an array that has too many items (E006 maxItems).
165pub 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
176/// Format a number the way JavaScript would with `String(n)`.
177fn 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
185/// Format a number for display in messages.
186fn 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
194/// Format a JSON value for display in messages (like JSON.stringify).
195fn 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}