use crate::frame;
#[derive(Debug, Default, Clone)]
pub struct CpBuilder {
fields: Vec<String>,
}
impl CpBuilder {
pub fn new() -> Self {
Self { fields: Vec::new() }
}
pub fn data_time(&mut self, data_time: impl AsRef<str>) -> &mut Self {
self.kv("DataTime", data_time);
self
}
pub fn kv(&mut self, key: impl AsRef<str>, value: impl AsRef<str>) -> &mut Self {
let k = key.as_ref().trim();
let v = value.as_ref().trim();
if k.is_empty() {
return self;
}
self.fields.push(format!("{}={}", k, v));
self
}
pub fn rtd_flag(
&mut self,
code: impl AsRef<str>,
rtd_value: impl AsRef<str>,
flag: impl AsRef<str>,
) -> &mut Self {
let c = code.as_ref().trim();
if c.is_empty() {
return self;
}
let rtd = rtd_value.as_ref().trim();
let flg = flag.as_ref().trim();
self.fields
.push(format!("{}-Rtd={},{}-Flag={}", c, rtd, c, flg));
self
}
pub fn build(&self) -> String {
if self.fields.is_empty() {
return String::new();
}
let mut out = self.fields.join(";");
out.push(';');
out
}
}
#[derive(Debug, Clone)]
pub struct PayloadBuilder {
qn: String,
st: String,
cn: String,
pw: String,
mn: String,
flag: String,
pnum: Option<String>,
pno: Option<String>,
cp_body: String,
}
impl PayloadBuilder {
pub fn new(qn: impl AsRef<str>, pw: impl AsRef<str>, mn: impl AsRef<str>, cp_body: impl Into<String>) -> Self {
Self {
qn: qn.as_ref().trim().to_string(),
st: "22".to_string(),
cn: "2011".to_string(),
pw: pw.as_ref().trim().to_string(),
mn: mn.as_ref().trim().to_string(),
flag: "7".to_string(),
pnum: None,
pno: None,
cp_body: cp_body.into(),
}
}
pub fn st(mut self, st: impl AsRef<str>) -> Self {
self.st = st.as_ref().trim().to_string();
self
}
pub fn cn(mut self, cn: impl AsRef<str>) -> Self {
self.cn = cn.as_ref().trim().to_string();
self
}
pub fn flag(mut self, flag: impl AsRef<str>) -> Self {
self.flag = flag.as_ref().trim().to_string();
self
}
pub fn pnum(mut self, pnum: impl AsRef<str>) -> Self {
self.pnum = Some(pnum.as_ref().trim().to_string());
self
}
pub fn pno(mut self, pno: impl AsRef<str>) -> Self {
self.pno = Some(pno.as_ref().trim().to_string());
self
}
pub fn payload(&self) -> String {
let mut out = format!(
"QN={};ST={};CN={};PW={};MN={};Flag={};",
self.qn, self.st, self.cn, self.pw, self.mn, self.flag
);
if let Some(pnum) = &self.pnum {
out.push_str(&format!("PNUM={};", pnum));
}
if let Some(pno) = &self.pno {
out.push_str(&format!("PNO={};", pno));
}
out.push_str(&format!("CP=&&{}&&", self.cp_body));
out
}
pub fn frame(&self) -> String {
frame::build_frame(&self.payload())
}
pub fn frame_standard(&self) -> String {
frame::build_frame_standard(&self.payload())
}
}
pub fn build_qn_rtn(qn: impl AsRef<str>, pw: impl AsRef<str>, mn: impl AsRef<str>, qn_rtn: impl AsRef<str>) -> PayloadBuilder {
let mut cp = CpBuilder::new();
cp.kv("QnRtn", qn_rtn);
PayloadBuilder::new(qn, pw, mn, cp.build())
.st("91")
.cn("9011")
.flag("8")
}
pub fn build_exe_rtn(qn: impl AsRef<str>, pw: impl AsRef<str>, mn: impl AsRef<str>, exe_rtn: impl AsRef<str>) -> PayloadBuilder {
let mut cp = CpBuilder::new();
cp.kv("ExeRtn", exe_rtn);
PayloadBuilder::new(qn, pw, mn, cp.build())
.st("91")
.cn("9012")
.flag("8")
}
pub fn build_data_ack(qn: impl AsRef<str>, pw: impl AsRef<str>, mn: impl AsRef<str>) -> PayloadBuilder {
PayloadBuilder::new(qn, pw, mn, String::new())
.st("91")
.cn("9014")
.flag("8")
}
pub fn build_notify_ack(qn: impl AsRef<str>, pw: impl AsRef<str>, mn: impl AsRef<str>) -> PayloadBuilder {
PayloadBuilder::new(qn, pw, mn, String::new())
.st("91")
.cn("9013")
.flag("8")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cp_builder_renders_with_trailing_semicolon() {
let mut cp = CpBuilder::new();
cp.data_time("20250101010101")
.rtd_flag("a21026", "12.3", "N")
.kv("X", "1");
let s = cp.build();
assert!(s.starts_with("DataTime=20250101010101;"));
assert!(s.contains("a21026-Rtd=12.3,a21026-Flag=N"));
assert!(s.ends_with(';'));
}
#[test]
fn payload_builder_wraps_cp_and_builds_frame() {
let mut cp = CpBuilder::new();
cp.data_time("20250101010101")
.rtd_flag("a21026", "12.3", "N");
let pb = PayloadBuilder::new("QN1", "123456", "ABC", cp.build());
let payload = pb.payload();
assert!(payload.contains("QN=QN1;"));
assert!(payload.contains("ST=22;"));
assert!(payload.contains("CN=2011;"));
assert!(payload.contains("PW=123456;"));
assert!(payload.contains("MN=ABC;"));
assert!(payload.contains("Flag=7;"));
assert!(payload.contains("CP=&&DataTime=20250101010101;"));
let frame = pb.frame();
assert!(frame.starts_with("##"));
assert!(frame.len() > 10);
}
#[test]
fn payload_builder_supports_pnum_pno_order() {
let pb = PayloadBuilder::new("QN1", "123456", "MN1", "".to_string())
.st("32")
.cn("2061")
.flag("11")
.pnum("2")
.pno("1");
let payload = pb.payload();
assert!(payload.contains("Flag=11;PNUM=2;PNO=1;CP=&&&&"));
}
#[test]
fn appendix_c_ack_payloads_match_shape() {
let qn_rtn = build_qn_rtn("20240601085857223", "123456", "010000A8900016F000169DC0", "1").payload();
assert!(qn_rtn.contains("ST=91;CN=9011;"));
assert!(qn_rtn.contains("Flag=8;"));
assert!(qn_rtn.contains("CP=&&QnRtn=1;&&"));
let exe_rtn = build_exe_rtn("20240601085857223", "123456", "010000A8900016F000169DC0", "1").payload();
assert!(exe_rtn.contains("ST=91;CN=9012;"));
assert!(exe_rtn.contains("CP=&&ExeRtn=1;&&"));
let ack = build_data_ack("20240601085857534", "123456", "010000A8900016F000169DC0").payload();
assert!(ack.contains("ST=91;CN=9014;"));
assert!(ack.ends_with("CP=&&&&"));
}
}