use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Discovery {
#[serde(default)]
pub contract_version: String,
#[serde(default)]
pub cassettes: Vec<DiscoveryEntry>,
#[serde(default)]
pub problems: Vec<Problem>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DiscoveryEntry {
pub name: String,
#[serde(default)]
pub version: Option<String>,
#[serde(default)]
pub display_name: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub route_prefix: String,
#[serde(default)]
pub openapi_path: String,
#[serde(default)]
pub openapi_status: String,
#[serde(default)]
pub manifest_digest: String,
}
impl DiscoveryEntry {
#[must_use]
pub fn has_spec(&self) -> bool {
!self.openapi_path.is_empty() && self.openapi_status != "missing"
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Problem {
#[serde(default)]
pub subject: String,
#[serde(default)]
pub reason: String,
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn a_document_decodes_from_the_servers_own_field_names() {
let document: Discovery = serde_json::from_str(
r#"{
"contract_version": "v1",
"cassettes": [{
"name": "summary",
"version": "0.3.1",
"display_name": "Summaries",
"description": "Rolling summaries",
"route_prefix": "/v1/cassettes/summary",
"tables": ["summary.summary"],
"config": [{"key": "llm.model", "type": "string", "required": false, "secret": false}],
"openapi_path": "/v1/cassettes/summary/openapi.json",
"openapi_status": "fresh",
"manifest_digest": "sha256:abc"
}],
"problems": [{"subject": "http://sidecar.invalid/openapi", "reason": "kind is required"}]
}"#,
)
.unwrap();
assert_eq!(document.contract_version, "v1");
assert_eq!(document.cassettes[0].name, "summary");
assert_eq!(
document.cassettes[0].openapi_path,
"/v1/cassettes/summary/openapi.json",
);
assert!(document.cassettes[0].has_spec());
assert_eq!(document.problems[0].reason, "kind is required");
}
#[test]
fn fields_this_client_does_not_model_do_not_break_the_decode() {
let document: Discovery = serde_json::from_str(
r#"{"contract_version":"v1","cassettes":[{"name":"x","route_prefix":"/v1/cassettes/x",
"openapi_path":"/v1/cassettes/x/openapi.json","openapi_status":"fresh",
"manifest_digest":"","a_field_from_the_future":7}],"problems":[]}"#,
)
.unwrap();
assert_eq!(document.cassettes[0].name, "x");
}
#[test]
fn an_empty_install_is_a_document_not_an_error() {
let document: Discovery =
serde_json::from_str(r#"{"contract_version":"v1","cassettes":[],"problems":[]}"#)
.unwrap();
assert!(document.cassettes.is_empty());
}
#[test]
fn a_cassette_whose_spec_core_has_not_fetched_yet_generates_nothing() {
let entry = DiscoveryEntry {
name: "summary".to_owned(),
openapi_path: "/v1/cassettes/summary/openapi.json".to_owned(),
openapi_status: "missing".to_owned(),
..Default::default()
};
assert!(!entry.has_spec());
let stale = DiscoveryEntry {
openapi_status: "stale".to_owned(),
..entry
};
assert!(stale.has_spec());
}
}