use super::utils::*;
use super::{BoundedValidationResult, JobMode, OpMode, OpenProtocolError, ValidationResult, ID};
use chrono::{DateTime, FixedOffset};
use lazy_static::*;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::HashMap;
use std::error::Error;
use std::net::IpAddr;
use std::str::FromStr;
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Operator<'a> {
pub operator_id: ID,
pub operator_name: Option<&'a str>,
}
impl<'a> Operator<'a> {
pub fn new(id: ID) -> Self {
Self { operator_id: id, operator_name: None }
}
pub fn new_with_name(id: ID, name: &'a str) -> Self {
Self { operator_name: Some(name), ..Self::new(id) }
}
pub fn validate(&self) -> ValidationResult {
check_optional_str_empty(&self.operator_name, "operator_name")
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GeoLocation {
pub geo_latitude: f64,
pub geo_longitude: f64,
}
impl GeoLocation {
pub fn new(latitude: f64, longitude: f64) -> Self {
GeoLocation { geo_latitude: latitude, geo_longitude: longitude }
}
pub fn validate(&self) -> ValidationResult {
check_f64(self.geo_latitude, "geo_latitude")?;
if !(-90.0..=90.0).contains(&self.geo_latitude) {
return Err(OpenProtocolError::ConstraintViolated(
format!("latitude ({}) must be between -90 and 90", self.geo_latitude).into(),
));
}
check_f64(self.geo_longitude, "geo_longitude")?;
if !(-180.0..=180.0).contains(&self.geo_longitude) {
return Err(OpenProtocolError::ConstraintViolated(
format!("longitude ({}) must be between -180 and 180", self.geo_longitude).into(),
));
}
Ok(())
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Controller<'a> {
pub controller_id: ID,
pub display_name: &'a str,
pub controller_type: &'a str,
pub version: &'a str,
pub model: &'a str,
#[serde(rename = "IP")]
pub address: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(flatten)]
pub geo_location: Option<GeoLocation>,
pub op_mode: OpMode,
pub job_mode: JobMode,
#[serde(skip_serializing_if = "HashMap::is_empty")]
#[serde(default)]
pub last_cycle_data: HashMap<&'a str, f64>,
#[serde(skip_serializing_if = "HashMap::is_empty")]
#[serde(default)]
pub variables: HashMap<&'a str, f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_connection_time: Option<DateTime<FixedOffset>>,
#[serde(flatten)]
#[serde(skip_serializing_if = "Option::is_none")]
pub operator: Option<Operator<'a>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(borrow)]
pub job_card_id: Option<Cow<'a, str>>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(borrow)]
pub mold_id: Option<Cow<'a, str>>,
}
impl<'a> Controller<'a> {
pub fn validate(&self) -> BoundedValidationResult<'a> {
check_str_empty(self.controller_type, "controller_type")?;
check_str_empty(self.display_name, "display_name")?;
check_str_empty(self.version, "version")?;
check_str_empty(self.model, "version")?;
check_optional_str_empty(&self.job_card_id, "job_card_id")?;
check_optional_str_empty(&self.mold_id, "mold_id")?;
if let Some(geo) = &self.geo_location {
geo.validate()?;
}
if let Some(op) = &self.operator {
op.validate()?;
}
check_str_empty(self.address, "address")?;
lazy_static! {
static ref IP_REGEX: Regex =
Regex::new(r#"^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}$"#).unwrap();
static ref TTY_REGEX: Regex = Regex::new(r#"^tty\w+$"#).unwrap();
static ref COM_REGEX: Regex = Regex::new(r#"^COM(\d+)$"#).unwrap();
}
if !IP_REGEX.is_match(self.address) {
if !TTY_REGEX.is_match(self.address) && !COM_REGEX.is_match(self.address) {
return Err(OpenProtocolError::InvalidField {
field: "ip".into(),
value: self.address.into(),
description: "".into(),
});
}
} else {
let (address, port) = self.address.split_at(self.address.find(':').unwrap());
let unspecified: bool;
match IpAddr::from_str(address) {
Ok(addr) => unspecified = addr.is_unspecified(),
Err(err) => {
return Err(OpenProtocolError::InvalidField {
field: "ip[address]".into(),
value: address.into(),
description: format!("{} ({})", address, err.description()).into(),
})
}
}
let port = &port[1..];
match u16::from_str(port) {
Ok(n) => {
if n == 0 && !unspecified {
return Err(OpenProtocolError::InvalidField {
field: "ip[port]".into(),
value: port.into(),
description: "IP port cannot be zero".into(),
});
} else if n > 0 && unspecified {
return Err(OpenProtocolError::InvalidField {
field: "ip[port]".into(),
value: port.into(),
description: "null IP must have zero port number".into(),
});
}
}
Err(err) => {
return Err(OpenProtocolError::InvalidField {
field: "ip[port]".into(),
value: port.into(),
description: err.description().to_string().into(),
})
}
}
}
Ok(())
}
}
impl Default for Controller<'_> {
fn default() -> Self {
Controller {
controller_id: ID::from_u32(1),
display_name: "Unknown",
controller_type: "Unknown",
version: "Unknown",
model: "Unknown",
address: "0.0.0.0:0",
geo_location: None,
op_mode: OpMode::Unknown,
job_mode: JobMode::Unknown,
job_card_id: None,
last_cycle_data: Default::default(),
variables: Default::default(),
last_connection_time: None,
operator: None,
mold_id: None,
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_controller_to_json() {
let c = Controller {
op_mode: OpMode::Automatic,
job_mode: JobMode::ID02,
operator: Some(Operator::new_with_name(ID::from_u32(123), "John")),
..Default::default()
};
c.validate().unwrap();
let serialized = serde_json::to_string(&c).unwrap();
assert_eq!(
r#"{"controllerId":1,"displayName":"Unknown","controllerType":"Unknown","version":"Unknown","model":"Unknown","IP":"0.0.0.0:0","opMode":"Automatic","jobMode":"ID02","operatorId":123,"operatorName":"John"}"#,
serialized);
}
#[test]
fn test_controller_from_json() {
let c: Controller = serde_json::from_str(r#"{"controllerId":1,"displayName":"Hello","controllerType":"Unknown","version":"Unknown","model":"Unknown","IP":"127.0.0.1:123","opMode":"Automatic","jobMode":"ID02","operatorId":123,"operatorName":"John"}"#).unwrap();
c.validate().unwrap();
assert_eq!(
r#"Controller { controller_id: 1, display_name: "Hello", controller_type: "Unknown", version: "Unknown", model: "Unknown", address: "127.0.0.1:123", geo_location: None, op_mode: Automatic, job_mode: ID02, last_cycle_data: {}, variables: {}, last_connection_time: None, operator: Some(Operator { operator_id: 123, operator_name: Some("John") }), job_card_id: None, mold_id: None }"#,
format!("{:?}", &c));
}
#[test]
fn test_controller_validate() {
let c: Controller = Default::default();
c.validate().unwrap();
}
#[test]
fn test_operator_validate() {
Operator { operator_id: ID::from_u32(123), operator_name: Some("John") }
.validate()
.unwrap();
}
#[test]
fn test_controller_validate_address() {
let mut c: Controller = Default::default();
c.address = "1.02.003.004:05";
c.validate().unwrap();
c.address = "1.02.003.004:0";
assert!(c.validate().is_err());
c.address = "0.0.0.0:0";
c.validate().unwrap();
c.address = "0.0.0.0:123";
assert!(c.validate().is_err());
c.address = "COM123";
c.validate().unwrap();
c.address = "ttyABC";
c.validate().unwrap();
}
}