bd-ilo: Implement error types and core data models

This commit is contained in:
teernisse
2026-02-12 12:37:08 -05:00
parent 24739cb270
commit 8289d3b89f
5 changed files with 424 additions and 3 deletions

112
src/core/cache.rs Normal file
View File

@@ -0,0 +1,112 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheMetadata {
pub alias: String,
pub url: Option<String>,
pub fetched_at: DateTime<Utc>,
pub last_accessed: DateTime<Utc>,
pub content_hash: String,
pub raw_hash: String,
pub etag: Option<String>,
pub last_modified: Option<String>,
pub spec_version: String,
pub spec_title: String,
pub endpoint_count: usize,
pub schema_count: usize,
pub raw_size_bytes: u64,
pub source_format: String,
pub index_version: u32,
pub generation: u64,
pub index_hash: String,
}
impl CacheMetadata {
pub fn is_stale(&self, stale_threshold_days: u32) -> bool {
let age = Utc::now() - self.fetched_at;
age.num_days() > i64::from(stale_threshold_days)
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration;
#[test]
fn test_cache_metadata_serialization_roundtrip() {
let meta = CacheMetadata {
alias: "petstore".into(),
url: Some("https://example.com/openapi.json".into()),
fetched_at: Utc::now(),
last_accessed: Utc::now(),
content_hash: "sha256:abc".into(),
raw_hash: "sha256:def".into(),
etag: Some("\"abc123\"".into()),
last_modified: Some("Wed, 21 Oct 2025 07:28:00 GMT".into()),
spec_version: "1.0.0".into(),
spec_title: "Petstore".into(),
endpoint_count: 19,
schema_count: 8,
raw_size_bytes: 42_000,
source_format: "json".into(),
index_version: 1,
generation: 1,
index_hash: "sha256:idx".into(),
};
let json = serde_json::to_string(&meta).unwrap();
let deserialized: CacheMetadata = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.alias, "petstore");
assert_eq!(deserialized.endpoint_count, 19);
}
#[test]
fn test_is_stale_fresh() {
let meta = CacheMetadata {
alias: "test".into(),
url: None,
fetched_at: Utc::now(),
last_accessed: Utc::now(),
content_hash: String::new(),
raw_hash: String::new(),
etag: None,
last_modified: None,
spec_version: String::new(),
spec_title: String::new(),
endpoint_count: 0,
schema_count: 0,
raw_size_bytes: 0,
source_format: "json".into(),
index_version: 1,
generation: 1,
index_hash: String::new(),
};
assert!(!meta.is_stale(30));
}
#[test]
fn test_is_stale_old() {
let meta = CacheMetadata {
alias: "test".into(),
url: None,
fetched_at: Utc::now() - Duration::days(31),
last_accessed: Utc::now(),
content_hash: String::new(),
raw_hash: String::new(),
etag: None,
last_modified: None,
spec_version: String::new(),
spec_title: String::new(),
endpoint_count: 0,
schema_count: 0,
raw_size_bytes: 0,
source_format: "json".into(),
index_version: 1,
generation: 1,
index_hash: String::new(),
};
assert!(meta.is_stale(30));
}
}

View File

@@ -1 +1,2 @@
// Core business logic modules
pub mod cache;
pub mod spec;

112
src/core/spec.rs Normal file
View File

@@ -0,0 +1,112 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpecIndex {
pub index_version: u32,
pub generation: u64,
pub content_hash: String,
pub openapi: String,
pub info: IndexInfo,
pub endpoints: Vec<IndexedEndpoint>,
pub schemas: Vec<IndexedSchema>,
pub tags: Vec<IndexedTag>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexInfo {
pub title: String,
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexedEndpoint {
pub path: String,
pub method: String,
pub summary: Option<String>,
pub description: Option<String>,
pub operation_id: Option<String>,
pub tags: Vec<String>,
pub deprecated: bool,
pub parameters: Vec<IndexedParam>,
pub request_body_required: bool,
pub request_body_content_types: Vec<String>,
pub security_schemes: Vec<String>,
pub security_required: bool,
pub operation_ptr: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexedParam {
pub name: String,
pub location: String,
pub required: bool,
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexedSchema {
pub name: String,
pub schema_ptr: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexedTag {
pub name: String,
pub description: Option<String>,
pub endpoint_count: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spec_index_serialization_roundtrip() {
let index = SpecIndex {
index_version: 1,
generation: 1,
content_hash: "sha256:abc123".into(),
openapi: "3.0.3".into(),
info: IndexInfo {
title: "Petstore".into(),
version: "1.0.0".into(),
},
endpoints: vec![IndexedEndpoint {
path: "/pet".into(),
method: "GET".into(),
summary: Some("List pets".into()),
description: None,
operation_id: Some("listPets".into()),
tags: vec!["pet".into()],
deprecated: false,
parameters: vec![IndexedParam {
name: "limit".into(),
location: "query".into(),
required: false,
description: Some("Max items".into()),
}],
request_body_required: false,
request_body_content_types: vec![],
security_schemes: vec!["api_key".into()],
security_required: true,
operation_ptr: "/paths/~1pet/get".into(),
}],
schemas: vec![IndexedSchema {
name: "Pet".into(),
schema_ptr: "/components/schemas/Pet".into(),
}],
tags: vec![IndexedTag {
name: "pet".into(),
description: Some("Pet operations".into()),
endpoint_count: 1,
}],
};
let json = serde_json::to_string(&index).unwrap();
let deserialized: SpecIndex = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.info.title, "Petstore");
assert_eq!(deserialized.endpoints.len(), 1);
assert_eq!(deserialized.schemas.len(), 1);
assert_eq!(deserialized.tags.len(), 1);
}
}