mockforge_mqtt/
spec_registry.rs

1use mockforge_core::protocol_abstraction::{
2    ProtocolRequest, ProtocolResponse, ResponseStatus, SpecOperation, SpecRegistry,
3    ValidationError, ValidationResult,
4};
5use mockforge_core::{templating, Error, Protocol, Result};
6use std::path::Path;
7use tracing::{debug, warn};
8
9use crate::fixtures::MqttFixtureRegistry;
10
11/// MQTT implementation of SpecRegistry
12pub struct MqttSpecRegistry {
13    fixture_registry: MqttFixtureRegistry,
14}
15
16impl Default for MqttSpecRegistry {
17    fn default() -> Self {
18        Self::new()
19    }
20}
21
22impl MqttSpecRegistry {
23    pub fn new() -> Self {
24        Self {
25            fixture_registry: MqttFixtureRegistry::new(),
26        }
27    }
28
29    pub fn add_fixture(&mut self, fixture: crate::fixtures::MqttFixture) {
30        self.fixture_registry.add_fixture(fixture);
31    }
32
33    pub fn find_fixture_by_topic(&self, topic: &str) -> Option<&crate::fixtures::MqttFixture> {
34        self.fixture_registry.find_by_topic(topic)
35    }
36
37    /// Load fixtures from a directory
38    pub fn load_fixtures<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
39        let path = path.as_ref();
40
41        if !path.exists() {
42            warn!("Fixtures directory does not exist: {:?}", path);
43            return Ok(());
44        }
45
46        let entries = std::fs::read_dir(path)?;
47
48        for entry in entries {
49            let entry = entry?;
50            let path = entry.path();
51
52            if path.is_file() {
53                let extension = path.extension().and_then(|s| s.to_str());
54
55                match extension {
56                    Some("yaml") | Some("yml") => {
57                        self.load_fixture_file(&path)?;
58                    }
59                    Some("json") => {
60                        self.load_fixture_file_json(&path)?;
61                    }
62                    _ => {
63                        debug!("Skipping non-fixture file: {:?}", path);
64                    }
65                }
66            }
67        }
68
69        Ok(())
70    }
71
72    fn load_fixture_file(&mut self, path: &Path) -> Result<()> {
73        use std::fs;
74
75        let content = fs::read_to_string(path)?;
76        let fixture: crate::fixtures::MqttFixture = serde_yaml::from_str(&content)?;
77        self.add_fixture(fixture);
78        Ok(())
79    }
80
81    fn load_fixture_file_json(&mut self, path: &Path) -> Result<()> {
82        use std::fs;
83
84        let content = fs::read_to_string(path)?;
85        let fixture: crate::fixtures::MqttFixture = serde_json::from_str(&content)?;
86        self.add_fixture(fixture);
87        Ok(())
88    }
89}
90
91impl SpecRegistry for MqttSpecRegistry {
92    fn protocol(&self) -> Protocol {
93        Protocol::Mqtt
94    }
95
96    fn operations(&self) -> Vec<SpecOperation> {
97        self.fixture_registry
98            .fixtures()
99            .map(|fixture| SpecOperation {
100                name: fixture.identifier.clone(),
101                path: fixture.topic_pattern.clone(),
102                operation_type: "PUBLISH".to_string(),
103                input_schema: None,
104                output_schema: Some(
105                    serde_json::to_string(&fixture.response.payload).unwrap_or_default(),
106                ),
107                metadata: std::collections::HashMap::new(),
108            })
109            .collect()
110    }
111
112    fn find_operation(&self, _operation: &str, path: &str) -> Option<SpecOperation> {
113        self.find_fixture_by_topic(path).map(|fixture| SpecOperation {
114            name: fixture.identifier.clone(),
115            path: fixture.topic_pattern.clone(),
116            operation_type: "PUBLISH".to_string(),
117            input_schema: None,
118            output_schema: Some(
119                serde_json::to_string(&fixture.response.payload).unwrap_or_default(),
120            ),
121            metadata: std::collections::HashMap::new(),
122        })
123    }
124
125    fn validate_request(&self, request: &ProtocolRequest) -> Result<ValidationResult> {
126        let topic = request.topic.as_ref();
127
128        if topic.is_none() {
129            return Ok(ValidationResult::failure(vec![ValidationError {
130                message: "Missing topic in MQTT request".to_string(),
131                path: Some("topic".to_string()),
132                code: Some("MISSING_TOPIC".to_string()),
133            }]));
134        }
135
136        let topic = topic.unwrap();
137        let valid = self.find_fixture_by_topic(topic).is_some();
138
139        if valid {
140            Ok(ValidationResult::success())
141        } else {
142            Ok(ValidationResult::failure(vec![ValidationError {
143                message: format!("No fixture found for topic: {}", topic),
144                path: Some("topic".to_string()),
145                code: Some("NO_FIXTURE".to_string()),
146            }]))
147        }
148    }
149
150    fn generate_mock_response(&self, request: &ProtocolRequest) -> Result<ProtocolResponse> {
151        let topic = request.topic.as_ref().ok_or_else(|| Error::Validation {
152            message: "Missing topic".to_string(),
153        })?;
154
155        let fixture = self.find_fixture_by_topic(topic).ok_or_else(|| Error::Routing {
156            message: format!("No fixture found for topic: {}", topic),
157        })?;
158
159        // Create templating context with environment variables
160        let mut env_vars = std::collections::HashMap::new();
161        env_vars.insert("topic".to_string(), topic.clone());
162
163        let context = templating::TemplatingContext::with_env(env_vars);
164
165        // Use template engine to render payload
166        let template_str = serde_json::to_string(&fixture.response.payload).map_err(Error::Json)?;
167        let expanded_payload = templating::expand_str_with_context(&template_str, &context);
168        let payload = expanded_payload.into_bytes();
169
170        Ok(ProtocolResponse {
171            status: ResponseStatus::MqttStatus(true),
172            metadata: std::collections::HashMap::from([
173                ("topic".to_string(), topic.clone()),
174                ("qos".to_string(), request.qos.unwrap_or(0).to_string()),
175                ("retained".to_string(), fixture.retained.to_string()),
176            ]),
177            body: payload,
178            content_type: "application/json".to_string(),
179        })
180    }
181}