use self::filters::*;
use self::utils::*;
use super::*;
use chrono::{DateTime, FixedOffset};
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::collections::HashMap;
use std::num::NonZeroU32;
use std::sync::atomic::{AtomicU64, Ordering};
use Message::*;
static SEQ: AtomicU64 = AtomicU64::new(1);
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct MessageOptions<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<&'a str>,
pub sequence: u64,
#[serde(skip_serializing_if = "is_zero")]
#[serde(default)]
pub priority: i32,
}
impl<'a> MessageOptions<'a> {
pub fn new() -> Self {
Default::default()
}
pub fn new_with_priority(priority: i32) -> Self {
Self { priority, ..Self::new() }
}
pub fn validate(&self) -> Result<'static, ()> {
check_optional_str_empty(&self.id, "id")?;
Ok(())
}
}
impl Default for MessageOptions<'_> {
fn default() -> Self {
Self { id: None, sequence: SEQ.fetch_add(1, Ordering::SeqCst), priority: 0 }
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct JobCard<'a> {
#[serde(borrow)]
pub job_card_id: Cow<'a, str>,
#[serde(borrow)]
pub mold_id: Cow<'a, str>,
pub progress: u32,
pub total: u32,
}
impl<'a> JobCard<'a> {
pub fn new(id: &'a str, mold: &'a str, progress: u32, total: u32) -> Self {
Self { job_card_id: id.into(), mold_id: mold.into(), progress, total }
}
pub fn validate(&self) -> Result<'static, ()> {
check_str_empty(&self.job_card_id, "job_card_id")?;
check_str_empty(&self.mold_id, "mold_id")?;
if self.progress > self.total {
return Err(OpenProtocolError::ConstraintViolated(
format!("JobCard progress ({}) must not be larger than total ({}).", self.progress, self.total).into(),
));
}
Ok(())
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KeyValuePair<K, V> {
pub key: K,
pub value: V,
}
impl<K, V> KeyValuePair<K, V> {
pub fn new(key: K, value: V) -> Self {
Self { key, value }
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StateValues<'a> {
#[serde(skip_serializing_if = "OpMode::is_unknown")]
#[serde(default)]
pub op_mode: OpMode,
#[serde(skip_serializing_if = "JobMode::is_unknown")]
#[serde(default)]
pub job_mode: JobMode,
#[serde(skip_serializing_if = "Option::is_none")]
pub operator_id: Option<NonZeroU32>,
#[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> StateValues<'a> {
pub fn new(op: OpMode, job: JobMode) -> Self {
Self { op_mode: op, job_mode: job, operator_id: None, job_card_id: None, mold_id: None }
}
pub fn new_with_all(
op: OpMode,
job: JobMode,
operator: Option<u32>,
job_card: Option<&'a str>,
mold: Option<&'a str>,
) -> Self {
Self {
operator_id: operator.map(|o| NonZeroU32::new(o).unwrap()),
job_card_id: job_card.map(|j| j.into()),
mold_id: mold.map(|m| m.into()),
..Self::new(op, job)
}
}
pub fn validate(&self) -> Result<'static, ()> {
check_optional_str_empty(&self.job_card_id, "job_card_id")?;
check_optional_str_empty(&self.mold_id, "mold_id")
}
}
impl Default for StateValues<'_> {
fn default() -> Self {
Self {
op_mode: OpMode::Unknown,
job_mode: JobMode::Unknown,
operator_id: None,
job_card_id: None,
mold_id: None,
}
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
#[serde(tag = "$type")]
pub enum Message<'a> {
#[serde(rename_all = "camelCase")]
Alive {
#[serde(flatten)]
options: MessageOptions<'a>,
},
#[serde(rename_all = "camelCase")]
ControllerAction {
controller_id: NonZeroU32,
action_id: i32,
timestamp: DateTime<FixedOffset>,
#[serde(flatten)]
options: MessageOptions<'a>,
},
#[serde(rename_all = "camelCase")]
RequestControllersList {
#[serde(skip_serializing_if = "Option::is_none")]
controller_id: Option<NonZeroU32>,
#[serde(flatten)]
options: MessageOptions<'a>,
},
#[serde(rename_all = "camelCase")]
ControllersList {
data: HashMap<&'a str, Controller<'a>>,
#[serde(flatten)]
options: MessageOptions<'a>,
},
#[serde(rename_all = "camelCase")]
ControllerStatus {
controller_id: NonZeroU32,
#[allow(clippy::option_option)]
#[serde(skip_serializing_if = "Option::is_none")]
display_name: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
is_connected: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
op_mode: Option<OpMode>,
#[serde(skip_serializing_if = "Option::is_none")]
job_mode: Option<JobMode>,
#[serde(skip_serializing_if = "Option::is_none")]
alarm: Option<KeyValuePair<&'a str, bool>>,
#[serde(skip_serializing_if = "Option::is_none")]
audit: Option<KeyValuePair<&'a str, f64>>,
#[serde(skip_serializing_if = "Option::is_none")]
variable: Option<KeyValuePair<&'a str, f64>>,
#[allow(clippy::option_option)]
#[serde(skip_serializing_if = "Option::is_none")]
operator_id: Option<Option<NonZeroU32>>,
#[allow(clippy::option_option)]
#[serde(deserialize_with = "deserialize_null_to_none")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
operator_name: Option<Option<&'a str>>,
#[allow(clippy::option_option)]
#[serde(deserialize_with = "deserialize_null_to_cow_none")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
#[serde(borrow)]
job_card_id: Option<Option<Cow<'a, str>>>,
#[allow(clippy::option_option)]
#[serde(deserialize_with = "deserialize_null_to_cow_none")]
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(default)]
#[serde(borrow)]
mold_id: Option<Option<Cow<'a, str>>>,
state: StateValues<'a>,
#[serde(skip_serializing_if = "Option::is_none")]
controller: Option<Box<Controller<'a>>>,
#[serde(flatten)]
options: MessageOptions<'a>,
},
#[serde(rename_all = "camelCase")]
CycleData {
controller_id: NonZeroU32,
data: HashMap<&'a str, f64>,
timestamp: DateTime<FixedOffset>,
#[serde(flatten)]
state: StateValues<'a>,
#[serde(flatten)]
options: MessageOptions<'a>,
},
#[serde(rename_all = "camelCase")]
RequestJobCardsList {
controller_id: NonZeroU32,
#[serde(flatten)]
options: MessageOptions<'a>,
},
#[serde(rename_all = "camelCase")]
JobCardsList {
controller_id: NonZeroU32,
data: HashMap<&'a str, JobCard<'a>>,
#[serde(flatten)]
options: MessageOptions<'a>,
},
#[serde(rename_all = "camelCase")]
Join {
#[serde(skip_serializing_if = "Option::is_none")]
org_id: Option<&'a str>,
version: &'a str,
password: &'a str,
language: Language,
#[serde(serialize_with = "serialize_to_flatten_array")]
#[serde(deserialize_with = "deserialize_flattened_array")]
#[serde(borrow)]
filter: Cow<'a, [Filter]>,
#[serde(flatten)]
options: MessageOptions<'a>,
},
#[serde(rename_all = "camelCase")]
JoinResponse {
result: u32,
#[serde(skip_serializing_if = "Option::is_none")]
level: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(borrow)]
message: Option<Cow<'a, str>>,
#[serde(flatten)]
options: MessageOptions<'a>,
},
#[serde(rename_all = "camelCase")]
RequestMoldData {
controller_id: NonZeroU32,
#[serde(flatten)]
options: MessageOptions<'a>,
},
#[serde(rename_all = "camelCase")]
MoldData {
controller_id: NonZeroU32,
data: HashMap<&'a str, f64>,
timestamp: DateTime<FixedOffset>,
#[serde(flatten)]
state: StateValues<'a>,
#[serde(flatten)]
options: MessageOptions<'a>,
},
#[serde(rename_all = "camelCase")]
ReadMoldData {
controller_id: NonZeroU32,
field: Option<&'a str>,
#[serde(flatten)]
options: MessageOptions<'a>,
},
#[serde(rename_all = "camelCase")]
MoldDataValue {
controller_id: NonZeroU32,
field: &'a str,
value: f64,
#[serde(flatten)]
options: MessageOptions<'a>,
},
#[serde(rename_all = "camelCase")]
LoginOperator {
controller_id: NonZeroU32,
password: &'a str,
#[serde(flatten)]
options: MessageOptions<'a>,
},
#[serde(rename_all = "camelCase")]
OperatorInfo {
controller_id: NonZeroU32,
#[serde(skip_serializing_if = "Option::is_none")]
operator_id: Option<NonZeroU32>,
name: &'a str,
password: &'a str,
level: u8,
#[serde(flatten)]
options: MessageOptions<'a>,
},
}
impl<'a> Message<'a> {
pub const PROTOCOL_VERSION: &'static str = "4.0";
pub const DEFAULT_LANGUAGE: Language = Language::EN;
pub const MAX_OPERATOR_LEVEL: u8 = 10;
pub fn parse_from_json_str(json: &'a str) -> Result<'a, Self> {
match serde_json::from_str::<Message>(json) {
Ok(m) => m.validate().map(|_| m),
Err(err) => Err(OpenProtocolError::JsonError(err)),
}
}
pub fn to_json_str(&self) -> Result<'_, String> {
self.validate()?;
serde_json::to_string(self).map_err(OpenProtocolError::JsonError)
}
pub fn new_alive() -> Self {
Alive { options: Default::default() }
}
pub fn new_join(password: &'a str, filter: &'a [Filter]) -> Self {
Self::new_join_with_org(password, filter, None)
}
pub fn new_join_with_org(password: &'a str, filter: &'a [Filter], org: Option<&'a str>) -> Self {
Join {
org_id: org,
version: Self::PROTOCOL_VERSION,
password,
language: Self::DEFAULT_LANGUAGE,
filter: filter.into(),
options: Default::default(),
}
}
pub fn validate(&self) -> Result<'a, ()> {
match self {
Alive { options, .. }
| ControllerAction { options, .. }
| RequestControllersList { options, .. }
| RequestJobCardsList { options, .. }
| JoinResponse { options, .. }
| RequestMoldData { options, .. } => options.validate(),
ControllersList { options, data, .. } => {
for c in data {
c.1.validate()?;
}
options.validate()
}
ControllerStatus {
options,
display_name,
alarm,
audit,
variable,
operator_name,
job_card_id,
mold_id,
state,
controller,
..
} => {
check_optional_str_empty(display_name, "display_name")?;
if let Some(x) = operator_name {
check_optional_str_whitespace(x, "operator_name")?;
}
if let Some(x) = job_card_id {
check_optional_str_whitespace(x, "job_card_id")?;
}
if let Some(x) = mold_id {
check_optional_str_whitespace(x, "mold_id")?;
}
state.validate()?;
if let Some(kv) = alarm {
check_str_empty(kv.key, "alarm.key")?;
}
if let Some(kv) = audit {
check_str_empty(kv.key, "audit.key")?;
check_f64(kv.value, "audit.value")?;
}
if let Some(kv) = variable {
check_str_empty(kv.key, "variable.key")?;
check_f64(kv.value, "variable.value")?;
}
if let Some(c) = controller {
c.validate()?;
}
options.validate()
}
CycleData { options, data, state, .. } => {
for d in data {
check_f64(*d.1, d.0)?;
}
check_optional_str_empty(&state.job_card_id, "job_card_id")?;
check_optional_str_empty(&state.mold_id, "mold_id")?;
options.validate()
}
JobCardsList { options, data, .. } => {
for jc in data {
jc.1.validate()?;
}
options.validate()
}
Join { options, org_id, version, password, language, filter, .. } => {
check_optional_str_empty(org_id, "org_id")?;
check_str_empty(version, "version")?;
check_str_empty(password, "password")?;
if *language == Language::Unknown {
return Err(OpenProtocolError::InvalidField {
field: "language".into(),
value: "Unknown".into(),
description: "Language cannot be Unknown.".into(),
});
}
let mut list: Vec<Filter> = filter.iter().cloned().collect();
list.dedup();
if filter.len() != list.len() {
return Err(OpenProtocolError::ConstraintViolated("filter list contains duplications.".into()));
}
options.validate()
}
MoldData { options, data, state, .. } => {
for d in data {
check_f64(*d.1, d.0)?;
}
check_optional_str_empty(&state.job_card_id, "job_card_id")?;
check_optional_str_empty(&state.mold_id, "mold_id")?;
options.validate()
}
ReadMoldData { options, field, .. } => {
check_optional_str_empty(field, "field")?;
options.validate()
}
MoldDataValue { options, field, value, .. } => {
check_str_empty(field, "field")?;
check_f64(*value, "value")?;
options.validate()
}
LoginOperator { options, password, .. } => {
check_str_empty(&password, "password")?;
options.validate()
}
OperatorInfo { options, name, password, level, .. } => {
check_str_empty(name, "name")?;
check_optional_str_whitespace(&Some(*password), "password")?;
if *level > Self::MAX_OPERATOR_LEVEL {
return Err(OpenProtocolError::ConstraintViolated(
format!("Level {} is too high - must be between 0 and {}.", level, Self::MAX_OPERATOR_LEVEL)
.into(),
));
}
options.validate()
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_alive() {
let m = Alive { options: MessageOptions { id: Some("Hello"), sequence: 999, priority: 20 } };
let serialized = serde_json::to_string(&m).unwrap();
assert_eq!(r#"{"$type":"Alive","id":"Hello","sequence":999,"priority":20}"#, serialized);
}
#[test]
fn test_mold_data() {
let mut map = HashMap::<&str, f64>::new();
map.insert("Hello", 123.0);
map.insert("World", -987.6543);
map.insert("foo", 0.0);
let m = MoldData {
controller_id: NonZeroU32::new(123).unwrap(),
data: map,
timestamp: DateTime::parse_from_rfc3339("2019-02-26T02:03:04+08:00").unwrap(),
state: StateValues {
job_card_id: Some("Hello World!".into()),
mold_id: None,
operator_id: Some(NonZeroU32::new(42).unwrap()),
op_mode: OpMode::SemiAutomatic,
job_mode: JobMode::Offline,
},
options: MessageOptions { id: None, sequence: 999, priority: -20 },
};
let serialized = serde_json::to_string(&m).unwrap();
assert_eq!(
r#"{"$type":"MoldData","controllerId":123,"data":{"foo":0.0,"Hello":123.0,"World":-987.6543},"timestamp":"2019-02-26T02:03:04+08:00","jobCardId":"Hello World!","operatorId":42,"opMode":"SemiAutomatic","jobMode":"Offline","sequence":999,"priority":-20}"#,
serialized
);
let m2: Message = serde_json::from_str(&serialized).unwrap();
m2.validate().unwrap();
assert_eq!(m, m2);
}
#[test]
fn test_controllers_list() {
let json = r#"{"$type":"ControllersList","data":{"12345":{"controllerId":12345,"displayName":"Hello","controllerType":"Ai12","version":"1.0.0","model":"JM128-Ai","IP":"192.168.5.1","opMode":"Manual","jobMode":"ID11","lastCycleData":{"Z_QDGODCNT":8567,"Z_QDCYCTIM":979,"Z_QDINJTIM":5450,"Z_QDPLSTIM":7156,"Z_QDINJENDPOS":8449,"Z_QDPLSENDPOS":2212,"Z_QDFLAG":8988,"Z_QDPRDCNT":65500,"Z_QDCOLTIM":4435,"Z_QDMLDOPNTIM":652,"Z_QDMLDCLSTIM":2908,"Z_QDVPPOS":4732,"Z_QDMLDOPNENDPOS":6677,"Z_QDMAXINJSPD":7133,"Z_QDMAXPLSRPM":641,"Z_QDNOZTEMP":6693,"Z_QDTEMPZ01":9964,"Z_QDTEMPZ02":7579,"Z_QDTEMPZ03":4035,"Z_QDTEMPZ04":5510,"Z_QDTEMPZ05":8460,"Z_QDTEMPZ06":9882,"Z_QDBCKPRS":2753,"Z_QDHLDTIM":9936},"lastConnectionTime":"2016-03-06T23:11:27.1442177+08:00"},"22334":{"controllerId":22334,"displayName":"World","controllerType":1,"version":"1.0.0","model":"JM128-Ai","IP":"192.168.5.2","opMode":"SemiAutomatic","jobMode":"ID12","lastCycleData":{"Z_QDGODCNT":6031,"Z_QDCYCTIM":7526,"Z_QDINJTIM":4896,"Z_QDPLSTIM":5196,"Z_QDINJENDPOS":1250,"Z_QDPLSENDPOS":8753,"Z_QDFLAG":3314,"Z_QDPRDCNT":65500,"Z_QDCOLTIM":3435,"Z_QDMLDOPNTIM":7854,"Z_QDMLDCLSTIM":4582,"Z_QDVPPOS":7504,"Z_QDMLDOPNENDPOS":7341,"Z_QDMAXINJSPD":7322,"Z_QDMAXPLSRPM":6024,"Z_QDNOZTEMP":3406,"Z_QDTEMPZ01":3067,"Z_QDTEMPZ02":9421,"Z_QDTEMPZ03":2080,"Z_QDTEMPZ04":8845,"Z_QDTEMPZ05":4478,"Z_QDTEMPZ06":3126,"Z_QDBCKPRS":2807,"Z_QDHLDTIM":3928},"lastConnectionTime":"2016-03-06T23:11:27.149218+08:00"}},"sequence":68568}"#;
let m: Message = serde_json::from_str(&json).unwrap();
m.validate().unwrap();
if let ControllersList { data, .. } = m {
assert_eq!(2, data.len());
let c = data.get("12345").unwrap();
assert_eq!("Hello", c.display_name);
} else {
panic!("Expected ControllersList, got {:#?}", m);
}
}
#[test]
fn test_cycle_data() {
let json = r#"{"$type":"CycleData","timestamp":"2016-02-26T01:12:23+08:00","opMode":"Automatic","jobMode":"ID02","controllerId":123,"data":{"Z_QDGODCNT":123,"Z_QDCYCTIM":12.33,"Z_QDINJTIM":3,"Z_QDPLSTIM":4.4,"Z_QDINJENDPOS":30.1,"Z_QDPLSENDPOS":20.3,"Z_QDFLAG":1,"Z_QDPRDCNT":500,"Z_QDCOLTIM":12.12,"Z_QDMLDOPNTIM":2.1,"Z_QDMLDCLSTIM":1.3,"Z_QDVPPOS":12.11,"Z_QDMLDOPNENDPOS":130.1,"Z_QDMAXINJSPD":213.12,"Z_QDMAXPLSRPM":551,"Z_QDNOZTEMP":256,"Z_QDTEMPZ01":251,"Z_QDTEMPZ02":252,"Z_QDTEMPZ03":253,"Z_QDTEMPZ04":254,"Z_QDTEMPZ05":255,"Z_QDTEMPZ06":256,"Z_QDBCKPRS":54,"Z_QDHLDTIM":2.3,"Z_QDCPT01":231,"Z_QDCPT02":232,"Z_QDCPT03":233,"Z_QDCPT04":234,"Z_QDCPT05":235,"Z_QDCPT06":236,"Z_QDCPT07":237,"Z_QDCPT08":238,"Z_QDCPT09":239,"Z_QDCPT10":240,"Z_QDCPT11":241,"Z_QDCPT12":242,"Z_QDCPT13":243,"Z_QDCPT14":244,"Z_QDCPT15":245,"Z_QDCPT16":246,"Z_QDCPT17":247,"Z_QDCPT18":248,"Z_QDCPT19":249,"Z_QDCPT20":250,"Z_QDCPT21":251,"Z_QDCPT22":252,"Z_QDCPT23":253,"Z_QDCPT24":254,"Z_QDCPT25":255,"Z_QDCPT26":256,"Z_QDCPT27":257,"Z_QDCPT28":258,"Z_QDCPT29":259,"Z_QDCPT30":260,"Z_QDCPT31":261,"Z_QDCPT32":262,"Z_QDCPT33":263,"Z_QDCPT34":264,"Z_QDCPT35":265,"Z_QDCPT36":266,"Z_QDCPT37":267,"Z_QDCPT38":268,"Z_QDCPT39":269,"Z_QDCPT40":270},"sequence":1}"#;
let m: Message = serde_json::from_str(&json).unwrap();
m.validate().unwrap();
if let CycleData { options, controller_id, data, .. } = m {
assert_eq!(0, options.priority);
assert_eq!(123, controller_id.get());
assert_eq!(64, data.len());
assert!((*data.get("Z_QDCPT13").unwrap() - 243.0).abs() < std::f64::EPSILON);
} else {
panic!("Expected CycleData, got {:#?}", m);
}
}
#[test]
fn test_controller_status() {
let json = r#"{"$type":"ControllerStatus","controllerId":123,"displayName":"Testing","opMode":"Automatic","jobMode":"ID05","jobCardId":"XYZ","moldId":"Mold-123","state":{"opMode":"Automatic","jobMode":"ID05","jobCardId":"XYZ","moldId":"Mold-123"},"controller":{"controllerId":123,"displayName":"Testing","controllerType":"Ai02","version":"2.2","model":"JM138Ai","IP":"192.168.1.1:12345","geoLatitude":123.0,"geoLongitude":-21.0,"opMode":"Automatic","jobMode":"ID05","jobCardId":"XYZ","lastCycleData":{"INJ":5,"CLAMP":400},"moldId":"Mold-123"},"sequence":1,"priority":50}"#;
let m: Message = serde_json::from_str(&json).unwrap();
m.validate().unwrap();
if let ControllerStatus { options, controller_id, display_name, controller, .. } = m {
assert_eq!(50, options.priority);
assert_eq!(1, options.sequence);
assert_eq!(123, controller_id.get());
assert_eq!(Some("Testing"), display_name);
let c = controller.unwrap();
assert_eq!("JM138Ai", c.model);
let d = c.last_cycle_data.unwrap();
assert!(c.operator.is_none());
assert_eq!(2, d.len());
assert!((*d.get("INJ").unwrap() - 5.0).abs() < std::f64::EPSILON);
} else {
panic!("Expected ControllerStatus, got {:#?}", m);
}
}
}