mockforge_mqtt/
spec_registry.rs1use 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
11pub 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 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 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 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}