1use std::collections::HashMap;
26
27use crate::config::DeviceConfig;
28use crate::error::{Error, Result};
29use crate::protocol::Protocol;
30use crate::tags::Tags;
31use crate::types::{Address, DataPointDef, DataType};
32
33#[derive(Debug, Clone)]
38pub struct DeviceConfigBuilder {
39 id: String,
40 name: String,
41 description: String,
42 protocol: Protocol,
43 address: Option<String>,
44 points: Vec<DataPointDef>,
45 metadata: HashMap<String, String>,
46 tags: Tags,
47 validation_enabled: bool,
48}
49
50impl DeviceConfigBuilder {
51 pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
58 Self {
59 id: id.into(),
60 name: name.into(),
61 description: String::new(),
62 protocol: Protocol::ModbusTcp,
63 address: None,
64 points: Vec::new(),
65 metadata: HashMap::new(),
66 tags: Tags::new(),
67 validation_enabled: true,
68 }
69 }
70
71 pub fn protocol(mut self, protocol: Protocol) -> Self {
73 self.protocol = protocol;
74 self
75 }
76
77 pub fn description(mut self, description: impl Into<String>) -> Self {
79 self.description = description.into();
80 self
81 }
82
83 pub fn address(mut self, address: impl Into<String>) -> Self {
85 self.address = Some(address.into());
86 self
87 }
88
89 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
91 self.metadata.insert(key.into(), value.into());
92 self
93 }
94
95 pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
97 self.metadata.extend(metadata);
98 self
99 }
100
101 pub fn tags(mut self, tags: Tags) -> Self {
103 self.tags = tags;
104 self
105 }
106
107 pub fn tag(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
109 self.tags.insert(key.into(), value.into());
110 self
111 }
112
113 pub fn label(mut self, label: impl Into<String>) -> Self {
115 self.tags.add_label(label.into());
116 self
117 }
118
119 pub fn labels<I, S>(mut self, labels: I) -> Self
121 where
122 I: IntoIterator<Item = S>,
123 S: Into<String>,
124 {
125 for label in labels {
126 self.tags.add_label(label.into());
127 }
128 self
129 }
130
131 pub fn add_point(mut self, point: DataPointDef) -> Self {
133 self.points.push(point);
134 self
135 }
136
137 pub fn add_points(mut self, points: impl IntoIterator<Item = DataPointDef>) -> Self {
139 self.points.extend(points);
140 self
141 }
142
143 pub fn skip_validation(mut self) -> Self {
145 self.validation_enabled = false;
146 self
147 }
148
149 pub fn validate(&self) -> Result<()> {
151 if self.id.is_empty() {
152 return Err(Error::Config("Device ID cannot be empty".into()));
153 }
154 if self.name.is_empty() {
155 return Err(Error::Config("Device name cannot be empty".into()));
156 }
157
158 let mut point_ids = std::collections::HashSet::new();
160 for point in &self.points {
161 if !point_ids.insert(&point.id) {
162 return Err(Error::Config(format!(
163 "Duplicate data point ID: {}",
164 point.id
165 )));
166 }
167 }
168
169 Ok(())
170 }
171
172 pub fn build(self) -> Result<DeviceConfig> {
174 if self.validation_enabled {
175 self.validate()?;
176 }
177
178 Ok(DeviceConfig {
179 id: self.id,
180 name: self.name,
181 description: self.description,
182 protocol: self.protocol,
183 address: self.address,
184 points: self
185 .points
186 .into_iter()
187 .map(|p| crate::config::DataPointConfig {
188 id: p.id,
189 name: p.name,
190 data_type: format!("{:?}", p.data_type).to_lowercase(),
191 access: format!("{:?}", p.access).to_lowercase(),
192 address: p.address.map(|a| format!("{:?}", a)),
193 initial_value: p.default_value.map(|v| serde_json::to_value(v).unwrap()),
194 units: p.units,
195 min: p.min_value,
196 max: p.max_value,
197 })
198 .collect(),
199 metadata: self.metadata,
200 tags: self.tags,
201 })
202 }
203}
204
205#[derive(Debug, Clone)]
207pub struct DataPointBuilder {
208 id: String,
209 name: String,
210 data_type: DataType,
211 description: String,
212 access: crate::types::AccessMode,
213 units: Option<String>,
214 min_value: Option<f64>,
215 max_value: Option<f64>,
216 default_value: Option<crate::value::Value>,
217 address: Option<Address>,
218}
219
220impl DataPointBuilder {
221 pub fn new(id: impl Into<String>, name: impl Into<String>, data_type: DataType) -> Self {
223 Self {
224 id: id.into(),
225 name: name.into(),
226 data_type,
227 description: String::new(),
228 access: crate::types::AccessMode::ReadWrite,
229 units: None,
230 min_value: None,
231 max_value: None,
232 default_value: None,
233 address: None,
234 }
235 }
236
237 pub fn description(mut self, description: impl Into<String>) -> Self {
239 self.description = description.into();
240 self
241 }
242
243 pub fn access(mut self, access: crate::types::AccessMode) -> Self {
245 self.access = access;
246 self
247 }
248
249 pub fn read_only(mut self) -> Self {
251 self.access = crate::types::AccessMode::ReadOnly;
252 self
253 }
254
255 pub fn write_only(mut self) -> Self {
257 self.access = crate::types::AccessMode::WriteOnly;
258 self
259 }
260
261 pub fn units(mut self, units: impl Into<String>) -> Self {
263 self.units = Some(units.into());
264 self
265 }
266
267 pub fn range(mut self, min: f64, max: f64) -> Self {
269 self.min_value = Some(min);
270 self.max_value = Some(max);
271 self
272 }
273
274 pub fn min(mut self, min: f64) -> Self {
276 self.min_value = Some(min);
277 self
278 }
279
280 pub fn max(mut self, max: f64) -> Self {
282 self.max_value = Some(max);
283 self
284 }
285
286 pub fn default_value(mut self, value: impl Into<crate::value::Value>) -> Self {
288 self.default_value = Some(value.into());
289 self
290 }
291
292 pub fn address(mut self, address: Address) -> Self {
294 self.address = Some(address);
295 self
296 }
297
298 pub fn build(self) -> DataPointDef {
300 DataPointDef {
301 id: self.id,
302 name: self.name,
303 description: self.description,
304 data_type: self.data_type,
305 access: self.access,
306 units: self.units,
307 min_value: self.min_value,
308 max_value: self.max_value,
309 default_value: self.default_value,
310 address: self.address,
311 }
312 }
313}
314
315impl From<DataPointBuilder> for DataPointDef {
316 fn from(builder: DataPointBuilder) -> Self {
317 builder.build()
318 }
319}
320
321pub fn device(id: impl Into<String>, name: impl Into<String>) -> DeviceConfigBuilder {
323 DeviceConfigBuilder::new(id, name)
324}
325
326pub fn point(id: impl Into<String>, name: impl Into<String>, data_type: DataType) -> DataPointBuilder {
328 DataPointBuilder::new(id, name, data_type)
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334
335 #[test]
336 fn test_device_config_builder() {
337 let config = DeviceConfigBuilder::new("dev-001", "Test Device")
338 .protocol(Protocol::ModbusTcp)
339 .description("A test device")
340 .metadata("location", "Building A")
341 .add_point(
342 DataPointBuilder::new("temp", "Temperature", DataType::Float32)
343 .units("°C")
344 .range(-40.0, 80.0)
345 .build(),
346 )
347 .build()
348 .unwrap();
349
350 assert_eq!(config.id, "dev-001");
351 assert_eq!(config.name, "Test Device");
352 assert_eq!(config.protocol, Protocol::ModbusTcp);
353 assert_eq!(config.points.len(), 1);
354 }
355
356 #[test]
357 fn test_device_config_builder_validation() {
358 let result = DeviceConfigBuilder::new("", "Test")
360 .build();
361 assert!(result.is_err());
362
363 let result = DeviceConfigBuilder::new("test", "")
365 .build();
366 assert!(result.is_err());
367
368 let result = DeviceConfigBuilder::new("dev-001", "Test")
370 .add_point(DataPointBuilder::new("temp", "Temp 1", DataType::Float32).build())
371 .add_point(DataPointBuilder::new("temp", "Temp 2", DataType::Float32).build())
372 .build();
373 assert!(result.is_err());
374 }
375
376 #[test]
377 fn test_data_point_builder() {
378 let point = DataPointBuilder::new("temp", "Temperature", DataType::Float32)
379 .description("Room temperature")
380 .units("°C")
381 .range(-40.0, 80.0)
382 .read_only()
383 .default_value(25.0f64)
384 .build();
385
386 assert_eq!(point.id, "temp");
387 assert_eq!(point.units, Some("°C".to_string()));
388 assert_eq!(point.min_value, Some(-40.0));
389 assert_eq!(point.max_value, Some(80.0));
390 assert_eq!(point.access, crate::types::AccessMode::ReadOnly);
391 }
392
393 #[test]
394 fn test_shorthand_functions() {
395 let config = device("dev-001", "Test")
396 .protocol(Protocol::BacnetIp)
397 .add_point(point("temp", "Temperature", DataType::Float32).build())
398 .build()
399 .unwrap();
400
401 assert_eq!(config.id, "dev-001");
402 assert_eq!(config.protocol, Protocol::BacnetIp);
403 }
404
405 #[test]
406 fn test_device_config_builder_with_tags() {
407 let config = DeviceConfigBuilder::new("dev-001", "Tagged Device")
408 .protocol(Protocol::ModbusTcp)
409 .tag("location", "building-a")
410 .tag("floor", "3")
411 .label("hvac")
412 .label("critical")
413 .build()
414 .unwrap();
415
416 assert_eq!(config.tags.get("location"), Some("building-a"));
417 assert_eq!(config.tags.get("floor"), Some("3"));
418 assert!(config.tags.has_label("hvac"));
419 assert!(config.tags.has_label("critical"));
420 }
421
422 #[test]
423 fn test_device_config_builder_with_tags_object() {
424 let tags = Tags::new()
425 .with_tag("env", "prod")
426 .with_label("monitored");
427
428 let config = DeviceConfigBuilder::new("dev-002", "Device with Tags")
429 .tags(tags)
430 .build()
431 .unwrap();
432
433 assert_eq!(config.tags.get("env"), Some("prod"));
434 assert!(config.tags.has_label("monitored"));
435 }
436}