1use std::collections::HashMap;
26
27use crate::config::DeviceConfig;
28use crate::error::{Error, Result};
29use crate::protocol::Protocol;
30use crate::types::{Address, DataPointDef, DataType};
31
32#[derive(Debug, Clone)]
37pub struct DeviceConfigBuilder {
38 id: String,
39 name: String,
40 description: String,
41 protocol: Protocol,
42 address: Option<String>,
43 points: Vec<DataPointDef>,
44 metadata: HashMap<String, String>,
45 validation_enabled: bool,
46}
47
48impl DeviceConfigBuilder {
49 pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
56 Self {
57 id: id.into(),
58 name: name.into(),
59 description: String::new(),
60 protocol: Protocol::ModbusTcp,
61 address: None,
62 points: Vec::new(),
63 metadata: HashMap::new(),
64 validation_enabled: true,
65 }
66 }
67
68 pub fn protocol(mut self, protocol: Protocol) -> Self {
70 self.protocol = protocol;
71 self
72 }
73
74 pub fn description(mut self, description: impl Into<String>) -> Self {
76 self.description = description.into();
77 self
78 }
79
80 pub fn address(mut self, address: impl Into<String>) -> Self {
82 self.address = Some(address.into());
83 self
84 }
85
86 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
88 self.metadata.insert(key.into(), value.into());
89 self
90 }
91
92 pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
94 self.metadata.extend(metadata);
95 self
96 }
97
98 pub fn add_point(mut self, point: DataPointDef) -> Self {
100 self.points.push(point);
101 self
102 }
103
104 pub fn add_points(mut self, points: impl IntoIterator<Item = DataPointDef>) -> Self {
106 self.points.extend(points);
107 self
108 }
109
110 pub fn skip_validation(mut self) -> Self {
112 self.validation_enabled = false;
113 self
114 }
115
116 pub fn validate(&self) -> Result<()> {
118 if self.id.is_empty() {
119 return Err(Error::Config("Device ID cannot be empty".into()));
120 }
121 if self.name.is_empty() {
122 return Err(Error::Config("Device name cannot be empty".into()));
123 }
124
125 let mut point_ids = std::collections::HashSet::new();
127 for point in &self.points {
128 if !point_ids.insert(&point.id) {
129 return Err(Error::Config(format!(
130 "Duplicate data point ID: {}",
131 point.id
132 )));
133 }
134 }
135
136 Ok(())
137 }
138
139 pub fn build(self) -> Result<DeviceConfig> {
141 if self.validation_enabled {
142 self.validate()?;
143 }
144
145 Ok(DeviceConfig {
146 id: self.id,
147 name: self.name,
148 description: self.description,
149 protocol: self.protocol,
150 address: self.address,
151 points: self
152 .points
153 .into_iter()
154 .map(|p| crate::config::DataPointConfig {
155 id: p.id,
156 name: p.name,
157 data_type: format!("{:?}", p.data_type).to_lowercase(),
158 access: format!("{:?}", p.access).to_lowercase(),
159 address: p.address.map(|a| format!("{:?}", a)),
160 initial_value: p.default_value.map(|v| serde_json::to_value(v).unwrap()),
161 units: p.units,
162 min: p.min_value,
163 max: p.max_value,
164 })
165 .collect(),
166 metadata: self.metadata,
167 })
168 }
169}
170
171#[derive(Debug, Clone)]
173pub struct DataPointBuilder {
174 id: String,
175 name: String,
176 data_type: DataType,
177 description: String,
178 access: crate::types::AccessMode,
179 units: Option<String>,
180 min_value: Option<f64>,
181 max_value: Option<f64>,
182 default_value: Option<crate::value::Value>,
183 address: Option<Address>,
184}
185
186impl DataPointBuilder {
187 pub fn new(id: impl Into<String>, name: impl Into<String>, data_type: DataType) -> Self {
189 Self {
190 id: id.into(),
191 name: name.into(),
192 data_type,
193 description: String::new(),
194 access: crate::types::AccessMode::ReadWrite,
195 units: None,
196 min_value: None,
197 max_value: None,
198 default_value: None,
199 address: None,
200 }
201 }
202
203 pub fn description(mut self, description: impl Into<String>) -> Self {
205 self.description = description.into();
206 self
207 }
208
209 pub fn access(mut self, access: crate::types::AccessMode) -> Self {
211 self.access = access;
212 self
213 }
214
215 pub fn read_only(mut self) -> Self {
217 self.access = crate::types::AccessMode::ReadOnly;
218 self
219 }
220
221 pub fn write_only(mut self) -> Self {
223 self.access = crate::types::AccessMode::WriteOnly;
224 self
225 }
226
227 pub fn units(mut self, units: impl Into<String>) -> Self {
229 self.units = Some(units.into());
230 self
231 }
232
233 pub fn range(mut self, min: f64, max: f64) -> Self {
235 self.min_value = Some(min);
236 self.max_value = Some(max);
237 self
238 }
239
240 pub fn min(mut self, min: f64) -> Self {
242 self.min_value = Some(min);
243 self
244 }
245
246 pub fn max(mut self, max: f64) -> Self {
248 self.max_value = Some(max);
249 self
250 }
251
252 pub fn default_value(mut self, value: impl Into<crate::value::Value>) -> Self {
254 self.default_value = Some(value.into());
255 self
256 }
257
258 pub fn address(mut self, address: Address) -> Self {
260 self.address = Some(address);
261 self
262 }
263
264 pub fn build(self) -> DataPointDef {
266 DataPointDef {
267 id: self.id,
268 name: self.name,
269 description: self.description,
270 data_type: self.data_type,
271 access: self.access,
272 units: self.units,
273 min_value: self.min_value,
274 max_value: self.max_value,
275 default_value: self.default_value,
276 address: self.address,
277 }
278 }
279}
280
281impl From<DataPointBuilder> for DataPointDef {
282 fn from(builder: DataPointBuilder) -> Self {
283 builder.build()
284 }
285}
286
287pub fn device(id: impl Into<String>, name: impl Into<String>) -> DeviceConfigBuilder {
289 DeviceConfigBuilder::new(id, name)
290}
291
292pub fn point(id: impl Into<String>, name: impl Into<String>, data_type: DataType) -> DataPointBuilder {
294 DataPointBuilder::new(id, name, data_type)
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 #[test]
302 fn test_device_config_builder() {
303 let config = DeviceConfigBuilder::new("dev-001", "Test Device")
304 .protocol(Protocol::ModbusTcp)
305 .description("A test device")
306 .metadata("location", "Building A")
307 .add_point(
308 DataPointBuilder::new("temp", "Temperature", DataType::Float32)
309 .units("°C")
310 .range(-40.0, 80.0)
311 .build(),
312 )
313 .build()
314 .unwrap();
315
316 assert_eq!(config.id, "dev-001");
317 assert_eq!(config.name, "Test Device");
318 assert_eq!(config.protocol, Protocol::ModbusTcp);
319 assert_eq!(config.points.len(), 1);
320 }
321
322 #[test]
323 fn test_device_config_builder_validation() {
324 let result = DeviceConfigBuilder::new("", "Test")
326 .build();
327 assert!(result.is_err());
328
329 let result = DeviceConfigBuilder::new("test", "")
331 .build();
332 assert!(result.is_err());
333
334 let result = DeviceConfigBuilder::new("dev-001", "Test")
336 .add_point(DataPointBuilder::new("temp", "Temp 1", DataType::Float32).build())
337 .add_point(DataPointBuilder::new("temp", "Temp 2", DataType::Float32).build())
338 .build();
339 assert!(result.is_err());
340 }
341
342 #[test]
343 fn test_data_point_builder() {
344 let point = DataPointBuilder::new("temp", "Temperature", DataType::Float32)
345 .description("Room temperature")
346 .units("°C")
347 .range(-40.0, 80.0)
348 .read_only()
349 .default_value(25.0f64)
350 .build();
351
352 assert_eq!(point.id, "temp");
353 assert_eq!(point.units, Some("°C".to_string()));
354 assert_eq!(point.min_value, Some(-40.0));
355 assert_eq!(point.max_value, Some(80.0));
356 assert_eq!(point.access, crate::types::AccessMode::ReadOnly);
357 }
358
359 #[test]
360 fn test_shorthand_functions() {
361 let config = device("dev-001", "Test")
362 .protocol(Protocol::BacnetIp)
363 .add_point(point("temp", "Temperature", DataType::Float32).build())
364 .build()
365 .unwrap();
366
367 assert_eq!(config.id, "dev-001");
368 assert_eq!(config.protocol, Protocol::BacnetIp);
369 }
370}