mockforge_mqtt/
fixtures.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4/// MQTT fixture for topic-based mocking
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct MqttFixture {
7    pub identifier: String,
8    pub name: String,
9    pub topic_pattern: String, // Regex pattern for topic matching
10    pub qos: u8,
11    pub retained: bool,
12    pub response: MqttResponse,
13    pub auto_publish: Option<AutoPublishConfig>,
14}
15
16/// Response configuration for MQTT fixtures
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct MqttResponse {
19    pub payload: serde_json::Value, // Template-enabled JSON payload
20}
21
22/// Auto-publish configuration
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct AutoPublishConfig {
25    pub enabled: bool,
26    pub interval_ms: u64,
27    pub count: Option<usize>, // None = infinite
28}
29
30/// MQTT fixture registry
31pub struct MqttFixtureRegistry {
32    fixtures: HashMap<String, MqttFixture>,
33}
34
35impl Default for MqttFixtureRegistry {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl MqttFixtureRegistry {
42    pub fn new() -> Self {
43        Self {
44            fixtures: HashMap::new(),
45        }
46    }
47
48    pub fn add_fixture(&mut self, fixture: MqttFixture) {
49        self.fixtures.insert(fixture.identifier.clone(), fixture);
50    }
51
52    pub fn get_fixture(&self, identifier: &str) -> Option<&MqttFixture> {
53        self.fixtures.get(identifier)
54    }
55
56    pub fn find_by_topic(&self, topic: &str) -> Option<&MqttFixture> {
57        for fixture in self.fixtures.values() {
58            if regex::Regex::new(&fixture.topic_pattern).ok()?.is_match(topic) {
59                return Some(fixture);
60            }
61        }
62        None
63    }
64
65    pub fn fixtures(&self) -> impl Iterator<Item = &MqttFixture> {
66        self.fixtures.values()
67    }
68
69    /// Load fixtures from a directory
70    pub fn load_from_directory(
71        &mut self,
72        path: &std::path::Path,
73    ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
74        if !path.exists() {
75            return Err(format!("Fixtures directory does not exist: {}", path.display()).into());
76        }
77
78        if !path.is_dir() {
79            return Err(format!("Path is not a directory: {}", path.display()).into());
80        }
81
82        let mut loaded_count = 0;
83
84        // Read all .json and .yaml files from the directory
85        for entry in std::fs::read_dir(path)? {
86            let entry = entry?;
87            let path = entry.path();
88
89            if path.is_file() {
90                if let Some(extension) = path.extension() {
91                    if extension == "json" || extension == "yaml" || extension == "yml" {
92                        match self.load_fixture_file(&path) {
93                            Ok(fixture) => {
94                                self.add_fixture(fixture);
95                                loaded_count += 1;
96                            }
97                            Err(e) => {
98                                eprintln!(
99                                    "Warning: Failed to load fixture from {}: {}",
100                                    path.display(),
101                                    e
102                                );
103                            }
104                        }
105                    }
106                }
107            }
108        }
109
110        println!("✅ Loaded {} MQTT fixtures from {}", loaded_count, path.display());
111        Ok(())
112    }
113
114    /// Load a single fixture file
115    fn load_fixture_file(
116        &self,
117        path: &std::path::Path,
118    ) -> Result<MqttFixture, Box<dyn std::error::Error + Send + Sync>> {
119        let content = std::fs::read_to_string(path)?;
120        let fixture: MqttFixture = if path.extension().unwrap_or_default() == "json" {
121            serde_json::from_str(&content)?
122        } else {
123            serde_yaml::from_str(&content)?
124        };
125        Ok(fixture)
126    }
127}