mockforge_tcp/
spec_registry.rs

1//! TCP spec registry for managing TCP fixtures
2
3use crate::fixtures::TcpFixture;
4use mockforge_core::Result;
5use std::collections::HashMap;
6use std::path::{Path, PathBuf};
7use tracing::{debug, info, warn};
8
9/// Registry for TCP fixtures
10#[derive(Debug, Clone)]
11pub struct TcpSpecRegistry {
12    fixtures: HashMap<String, TcpFixture>,
13}
14
15impl TcpSpecRegistry {
16    /// Create a new empty registry
17    pub fn new() -> Self {
18        Self {
19            fixtures: HashMap::new(),
20        }
21    }
22
23    /// Load fixtures from a directory
24    pub fn load_fixtures<P: AsRef<Path>>(&mut self, fixtures_dir: P) -> Result<()> {
25        let fixtures_dir = fixtures_dir.as_ref();
26        if !fixtures_dir.exists() {
27            debug!("TCP fixtures directory does not exist: {:?}", fixtures_dir);
28            return Ok(());
29        }
30
31        info!("Loading TCP fixtures from {:?}", fixtures_dir);
32
33        let entries = std::fs::read_dir(fixtures_dir).map_err(|e| {
34            mockforge_core::Error::generic(format!("Failed to read fixtures directory: {}", e))
35        })?;
36
37        let mut loaded_count = 0;
38
39        for entry in entries {
40            let entry = entry.map_err(|e| {
41                mockforge_core::Error::generic(format!("Failed to read directory entry: {}", e))
42            })?;
43            let path = entry.path();
44
45            if path.is_file() {
46                match path.extension().and_then(|s| s.to_str()) {
47                    Some("yaml") | Some("yml") | Some("json") => {
48                        if let Err(e) = self.load_fixture_file(&path) {
49                            warn!("Failed to load fixture from {:?}: {}", path, e);
50                        } else {
51                            loaded_count += 1;
52                        }
53                    }
54                    _ => {
55                        debug!("Skipping non-fixture file: {:?}", path);
56                    }
57                }
58            }
59        }
60
61        info!("Loaded {} TCP fixture(s)", loaded_count);
62        Ok(())
63    }
64
65    /// Load a single fixture file
66    fn load_fixture_file<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
67        let path = path.as_ref();
68        let content = std::fs::read_to_string(path).map_err(|e| {
69            mockforge_core::Error::generic(format!("Failed to read fixture file: {}", e))
70        })?;
71
72        let fixtures: Vec<TcpFixture> = if path.extension().and_then(|s| s.to_str()) == Some("json")
73        {
74            serde_json::from_str(&content).map_err(|e| {
75                mockforge_core::Error::generic(format!("Failed to parse JSON fixture: {}", e))
76            })?
77        } else {
78            serde_yaml::from_str(&content).map_err(|e| {
79                mockforge_core::Error::generic(format!("Failed to parse YAML fixture: {}", e))
80            })?
81        };
82
83        for fixture in fixtures {
84            let identifier = fixture.identifier.clone();
85            self.fixtures.insert(identifier, fixture);
86        }
87
88        Ok(())
89    }
90
91    /// Add a fixture to the registry
92    pub fn add_fixture(&mut self, fixture: TcpFixture) {
93        let identifier = fixture.identifier.clone();
94        self.fixtures.insert(identifier, fixture);
95    }
96
97    /// Get a fixture by identifier
98    pub fn get_fixture(&self, identifier: &str) -> Option<&TcpFixture> {
99        self.fixtures.get(identifier)
100    }
101
102    /// Find a fixture matching the given data
103    pub fn find_matching_fixture(&self, data: &[u8]) -> Option<&TcpFixture> {
104        // Try to match against all fixtures
105        for fixture in self.fixtures.values() {
106            if fixture.matches(data) {
107                return Some(fixture);
108            }
109        }
110
111        None
112    }
113
114    /// Get all fixtures
115    pub fn get_all_fixtures(&self) -> Vec<&TcpFixture> {
116        self.fixtures.values().collect()
117    }
118
119    /// Remove a fixture
120    pub fn remove_fixture(&mut self, identifier: &str) -> Option<TcpFixture> {
121        self.fixtures.remove(identifier)
122    }
123
124    /// Clear all fixtures
125    pub fn clear(&mut self) {
126        self.fixtures.clear();
127    }
128}
129
130impl Default for TcpSpecRegistry {
131    fn default() -> Self {
132        Self::new()
133    }
134}
135
136impl TcpFixture {
137    /// Check if this fixture matches the given data
138    pub fn matches(&self, data: &[u8]) -> bool {
139        let criteria = &self.match_criteria;
140
141        // Check length constraints
142        if let Some(min_len) = criteria.min_length {
143            if data.len() < min_len {
144                return false;
145            }
146        }
147
148        if let Some(max_len) = criteria.max_length {
149            if data.len() > max_len {
150                return false;
151            }
152        }
153
154        // Match all - always matches
155        if criteria.match_all {
156            return true;
157        }
158
159        // Match by exact bytes
160        if let Some(ref exact_bytes_b64) = criteria.exact_bytes {
161            if let Ok(expected) = base64::decode(exact_bytes_b64) {
162                return data == expected.as_slice();
163            }
164        }
165
166        // Match by hex pattern
167        if let Some(ref hex_pattern) = criteria.data_pattern {
168            if let Ok(expected) = hex::decode(hex_pattern) {
169                return data == expected.as_slice();
170            }
171        }
172
173        // Match by text pattern (regex)
174        if let Some(ref text_pattern) = criteria.text_pattern {
175            if let Ok(re) = regex::Regex::new(text_pattern) {
176                if let Ok(text) = String::from_utf8(data.to_vec()) {
177                    return re.is_match(&text);
178                }
179            }
180        }
181
182        false
183    }
184}