use crate::Result;
use serde::de::DeserializeOwned;
use std::collections::HashMap;
pub trait ProtocolFixture: Send + Sync + Clone + std::fmt::Debug {
fn identifier(&self) -> &str;
fn lookup_key(&self) -> String;
fn operation_type(&self) -> &str;
fn output_schema(&self) -> Option<&str>;
fn input_schema(&self) -> Option<&str>;
fn metadata(&self) -> HashMap<String, String>;
}
#[derive(Debug, Clone)]
pub struct BaseSpecRegistry<F: ProtocolFixture> {
fixtures: Vec<F>,
key_index: HashMap<String, usize>,
id_index: HashMap<String, usize>,
}
impl<F: ProtocolFixture> BaseSpecRegistry<F> {
pub fn new() -> Self {
Self {
fixtures: Vec::new(),
key_index: HashMap::new(),
id_index: HashMap::new(),
}
}
pub fn add_fixture(&mut self, fixture: F) {
let idx = self.fixtures.len();
self.key_index.insert(fixture.lookup_key(), idx);
self.id_index.insert(fixture.identifier().to_string(), idx);
self.fixtures.push(fixture);
}
pub fn find_by_key(&self, key: &str) -> Option<&F> {
self.key_index.get(key).map(|&idx| &self.fixtures[idx])
}
pub fn find_by_identifier(&self, identifier: &str) -> Option<&F> {
self.id_index.get(identifier).map(|&idx| &self.fixtures[idx])
}
pub fn fixtures(&self) -> impl Iterator<Item = &F> {
self.fixtures.iter()
}
pub fn operations(&self) -> Vec<super::SpecOperation> {
self.fixtures
.iter()
.map(|f| super::SpecOperation {
name: f.identifier().to_string(),
path: f.lookup_key(),
operation_type: f.operation_type().to_string(),
input_schema: f.input_schema().map(String::from),
output_schema: f.output_schema().map(String::from),
metadata: f.metadata(),
})
.collect()
}
pub fn find_operation(&self, operation: &str, path: &str) -> Option<super::SpecOperation> {
self.fixtures.iter().find_map(|f| {
if f.operation_type() == operation && f.lookup_key().contains(path) {
Some(super::SpecOperation {
name: f.identifier().to_string(),
path: f.lookup_key(),
operation_type: f.operation_type().to_string(),
input_schema: f.input_schema().map(String::from),
output_schema: f.output_schema().map(String::from),
metadata: f.metadata(),
})
} else {
None
}
})
}
pub fn len(&self) -> usize {
self.fixtures.len()
}
pub fn is_empty(&self) -> bool {
self.fixtures.is_empty()
}
}
impl<F: ProtocolFixture + DeserializeOwned> BaseSpecRegistry<F> {
pub fn load_fixtures(&mut self, path: &std::path::Path) -> Result<usize> {
let content = std::fs::read_to_string(path).map_err(|e| {
crate::Error::internal(format!("Failed to read fixture file {}: {}", path.display(), e))
})?;
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
let fixtures: Vec<F> = match ext {
"yaml" | "yml" => serde_yaml::from_str(&content).map_err(|e| {
crate::Error::config(format!("Failed to parse YAML fixtures: {}", e))
})?,
"json" => serde_json::from_str(&content).map_err(|e| {
crate::Error::config(format!("Failed to parse JSON fixtures: {}", e))
})?,
_ => {
return Err(crate::Error::internal(format!(
"Unsupported fixture file extension: {ext}"
)));
}
};
let count = fixtures.len();
for f in fixtures {
self.add_fixture(f);
}
Ok(count)
}
}
impl<F: ProtocolFixture> Default for BaseSpecRegistry<F> {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
struct TestFixture {
id: String,
key: String,
op_type: String,
}
impl ProtocolFixture for TestFixture {
fn identifier(&self) -> &str {
&self.id
}
fn lookup_key(&self) -> String {
self.key.clone()
}
fn operation_type(&self) -> &str {
&self.op_type
}
fn output_schema(&self) -> Option<&str> {
None
}
fn input_schema(&self) -> Option<&str> {
None
}
fn metadata(&self) -> HashMap<String, String> {
HashMap::new()
}
}
fn sample_fixture(id: &str, key: &str, op: &str) -> TestFixture {
TestFixture {
id: id.to_string(),
key: key.to_string(),
op_type: op.to_string(),
}
}
#[test]
fn test_new_registry_is_empty() {
let reg = BaseSpecRegistry::<TestFixture>::new();
assert!(reg.is_empty());
assert_eq!(reg.len(), 0);
}
#[test]
fn test_add_and_find_by_key() {
let mut reg = BaseSpecRegistry::new();
reg.add_fixture(sample_fixture("get_users", "GET /users", "GET"));
assert_eq!(reg.len(), 1);
assert!(!reg.is_empty());
let found = reg.find_by_key("GET /users");
assert!(found.is_some());
assert_eq!(found.map(|f| f.identifier()), Some("get_users"));
}
#[test]
fn test_find_by_identifier() {
let mut reg = BaseSpecRegistry::new();
reg.add_fixture(sample_fixture("get_users", "GET /users", "GET"));
let found = reg.find_by_identifier("get_users");
assert!(found.is_some());
}
#[test]
fn test_find_missing_returns_none() {
let reg = BaseSpecRegistry::<TestFixture>::new();
assert!(reg.find_by_key("nonexistent").is_none());
assert!(reg.find_by_identifier("nonexistent").is_none());
}
#[test]
fn test_fixtures_iterator() {
let mut reg = BaseSpecRegistry::new();
reg.add_fixture(sample_fixture("a", "GET /a", "GET"));
reg.add_fixture(sample_fixture("b", "POST /b", "POST"));
let ids: Vec<&str> = reg.fixtures().map(|f| f.identifier()).collect();
assert_eq!(ids, vec!["a", "b"]);
}
#[test]
fn test_operations() {
let mut reg = BaseSpecRegistry::new();
reg.add_fixture(sample_fixture("get_users", "GET /users", "GET"));
reg.add_fixture(sample_fixture("post_users", "POST /users", "POST"));
let ops = reg.operations();
assert_eq!(ops.len(), 2);
assert_eq!(ops[0].operation_type, "GET");
assert_eq!(ops[1].operation_type, "POST");
}
#[test]
fn test_find_operation() {
let mut reg = BaseSpecRegistry::new();
reg.add_fixture(sample_fixture("get_users", "GET /users", "GET"));
let op = reg.find_operation("GET", "/users");
assert!(op.is_some());
assert_eq!(op.as_ref().map(|o| o.name.as_str()), Some("get_users"));
let missing = reg.find_operation("DELETE", "/users");
assert!(missing.is_none());
}
#[test]
fn test_load_fixtures_json() {
let dir = tempfile::tempdir().expect("create tempdir");
let path = dir.path().join("fixtures.json");
let fixtures = vec![
sample_fixture("a", "GET /a", "GET"),
sample_fixture("b", "POST /b", "POST"),
];
std::fs::write(&path, serde_json::to_string(&fixtures).expect("serialize")).expect("write");
let mut reg = BaseSpecRegistry::<TestFixture>::new();
let count = reg.load_fixtures(&path).expect("load");
assert_eq!(count, 2);
assert_eq!(reg.len(), 2);
}
#[test]
fn test_load_fixtures_yaml() {
let dir = tempfile::tempdir().expect("create tempdir");
let path = dir.path().join("fixtures.yaml");
let fixtures = vec![sample_fixture("x", "GET /x", "GET")];
std::fs::write(&path, serde_yaml::to_string(&fixtures).expect("serialize")).expect("write");
let mut reg = BaseSpecRegistry::<TestFixture>::new();
let count = reg.load_fixtures(&path).expect("load");
assert_eq!(count, 1);
assert!(reg.find_by_identifier("x").is_some());
}
#[test]
fn test_load_fixtures_unsupported_extension() {
let dir = tempfile::tempdir().expect("create tempdir");
let path = dir.path().join("fixtures.txt");
std::fs::write(&path, "irrelevant").expect("write");
let mut reg = BaseSpecRegistry::<TestFixture>::new();
assert!(reg.load_fixtures(&path).is_err());
}
#[test]
fn test_default_is_empty() {
let reg = BaseSpecRegistry::<TestFixture>::default();
assert!(reg.is_empty());
}
}