1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
pub mod lint;
pub mod operations;
/// Embedded validation rules for different OpenAPI versions
pub mod references;
pub mod schemas;
/// Basic validation rules that can be checked without external tools
pub mod basic {
use crate::core::validation::types::{Severity, ValidationError, ValidationResult};
use serde_json::Value;
/// Perform basic structural validation
pub fn validate_basic_structure(spec: &Value) -> ValidationResult {
let mut result = ValidationResult::new();
// Check for version field
if !spec.get("swagger").is_some() && !spec.get("openapi").is_some() {
result.errors.push(
ValidationError::new(
"missing-version",
"Specification must have either 'swagger' or 'openapi' field",
)
.with_path("$"),
);
}
// Check for info section
if spec.get("info").is_none() {
result.errors.push(
ValidationError::new("missing-info", "Specification must have an 'info' section")
.with_path("$"),
);
} else if let Some(info) = spec.get("info") {
// Check info has required fields
if info.get("title").is_none() {
result.errors.push(
ValidationError::new("missing-title", "Info section must have a 'title'")
.with_path("$.info"),
);
}
if info.get("version").is_none() {
result.errors.push(
ValidationError::new("missing-version", "Info section must have a 'version'")
.with_path("$.info"),
);
}
}
// Check for API content
let has_paths = spec.get("paths").is_some();
let has_webhooks = spec.get("webhooks").is_some();
let has_components = spec.get("components").is_some();
if !has_paths && !has_webhooks && !has_components {
result.errors.push(
ValidationError::new(
"missing-api-content",
"Specification must have at least one of: paths, webhooks, or components",
)
.with_path("$"),
);
}
result
}
/// Validate security aspects
pub fn validate_security(spec: &Value) -> ValidationResult {
let mut result = ValidationResult::new();
// Check servers for HTTPS
if let Some(servers) = spec.get("servers").and_then(|s| s.as_array()) {
for (i, server) in servers.iter().enumerate() {
if let Some(url) = server.get("url").and_then(|u| u.as_str()) {
let url_lower = url.to_lowercase();
// Check for HTTPS
if url_lower.starts_with("http://") && !url_lower.contains("localhost") {
result.warnings.push(
ValidationError::new(
"insecure-server",
format!("Server URL should use HTTPS instead of HTTP: {}", url),
)
.with_path(&format!("$.servers[{}].url", i))
.with_severity(Severity::Warning),
);
}
// Check for localhost/private IPs
if url_lower.contains("localhost")
|| url_lower.contains("127.0.0.1")
|| url_lower.contains("192.168.")
|| url_lower.contains("10.0.")
|| url_lower.contains("172.16.")
{
result.errors.push(
ValidationError::new(
"private-server-url",
format!("Server URL contains private/local address: {}", url),
)
.with_path(&format!("$.servers[{}].url", i)),
);
}
// Check for metadata endpoints
if url_lower.contains("169.254.169.254") || url_lower.contains("metadata") {
result.errors.push(
ValidationError::new(
"metadata-endpoint",
format!("Server URL points to metadata endpoint: {}", url),
)
.with_path(&format!("$.servers[{}].url", i)),
);
}
}
}
}
// Check for Swagger 2.0 schemes
if let Some(schemes) = spec.get("schemes").and_then(|s| s.as_array()) {
let has_https = schemes.iter().any(|s| s.as_str() == Some("https"));
if !has_https {
result.warnings.push(
ValidationError::new(
"no-https-scheme",
"API should support HTTPS scheme for security",
)
.with_path("$.schemes")
.with_severity(Severity::Warning),
);
}
}
result
}
/// Check for MicroRapid-specific requirements
pub fn validate_mrapids_requirements(spec: &Value) -> ValidationResult {
let mut result = ValidationResult::new();
// Check all operations have operationId
if let Some(paths) = spec.get("paths").and_then(|p| p.as_object()) {
for (path, path_item) in paths {
if let Some(path_obj) = path_item.as_object() {
for (method, operation) in path_obj {
// Skip non-operation fields
if ["parameters", "servers", "$ref"].contains(&method.as_str()) {
continue;
}
if let Some(op) = operation.as_object() {
if op.get("operationId").is_none() {
result.errors.push(
ValidationError::new(
"missing-operation-id",
"Operation must have operationId for CLI usage",
)
.with_path(&format!("$.paths.{}.{}", path, method)),
);
}
}
}
}
}
}
result
}
}