use std::collections::HashMap;
use serde_json::Value;
mod endpoints;
mod schemas;
mod validation;
fn normalize(path: &str) -> String {
let mut out = String::new();
let mut in_param = false;
for ch in path.chars() {
match ch {
'{' => {
in_param = true;
out.push_str("{p}");
}
'}' => in_param = false,
_ if in_param => {}
c => out.push(c),
}
}
out
}
pub async fn fetch_full_spec() -> Value {
let client = reqwest::Client::builder()
.user_agent("curl/8.0.0")
.build()
.expect("failed to build HTTP client");
client
.get("https://api.squarecloud.app/v2/openapi.json")
.send()
.await
.expect("failed to fetch OpenAPI spec")
.json()
.await
.expect("failed to parse OpenAPI spec")
}
pub async fn fetch_spec() -> HashMap<String, Value> {
let spec = fetch_full_spec().await;
spec["paths"]
.as_object()
.expect("OpenAPI spec missing 'paths'")
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
}
pub fn check(
paths: &HashMap<String, Value>,
method: &str,
our_path: &str,
) -> Option<String> {
let with_version = format!("/v2{our_path}");
let our_normalized = normalize(&with_version);
let spec_paths: HashMap<String, &str> =
paths.keys().map(|k| (normalize(k), k.as_str())).collect();
match spec_paths.get(&our_normalized) {
None => Some(format!("path not in spec: {our_path}")),
Some(&spec_path) => {
if paths[spec_path].get(method).is_none() {
let available = paths[spec_path]
.as_object()
.map(|o| o.keys().cloned().collect::<Vec<_>>().join(", "))
.unwrap_or_default();
Some(format!(
"method not in spec: {method} {our_path} \
(spec has: {available})"
))
} else {
None
}
}
}
}
#[test]
fn normalize_replaces_params() {
assert_eq!(normalize("/v2/apps/{appId}"), "/v2/apps/{p}");
assert_eq!(
normalize("/v2/apps/{appId}/network/logs"),
"/v2/apps/{p}/network/logs"
);
assert_eq!(normalize("/v2/apps/status"), "/v2/apps/status");
}