1#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum FieldKind {
14 Alphanumeric,
16 Numeric { scale: usize, signed: bool },
18 Group(Vec<FieldDecl>),
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct FieldDecl {
27 pub name: String,
29 pub pic: String,
31 pub offset: usize,
33 pub length: usize,
35 pub kind: FieldKind,
37}
38
39impl FieldDecl {
40 pub fn alnum(name: impl Into<String>, pic: impl Into<String>, offset: usize, length: usize) -> Self {
42 FieldDecl { name: name.into(), pic: pic.into(), offset, length, kind: FieldKind::Alphanumeric }
43 }
44
45 pub fn numeric(
47 name: impl Into<String>,
48 pic: impl Into<String>,
49 offset: usize,
50 length: usize,
51 scale: usize,
52 signed: bool,
53 ) -> Self {
54 FieldDecl {
55 name: name.into(),
56 pic: pic.into(),
57 offset,
58 length,
59 kind: FieldKind::Numeric { scale, signed },
60 }
61 }
62
63 pub fn group(name: impl Into<String>, offset: usize, children: Vec<FieldDecl>) -> Self {
65 let length = children.iter().map(|c| c.length).sum();
66 FieldDecl { name: name.into(), pic: String::new(), offset, length, kind: FieldKind::Group(children) }
67 }
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct Copybook {
74 pub record_name: String,
76 pub encoding: String,
79 pub fields: Vec<FieldDecl>,
81}
82
83impl Copybook {
84 pub fn record_length(&self) -> usize {
86 self.fields.iter().map(|f| f.length).sum()
87 }
88
89 pub fn leaf_fields(&self) -> Vec<&FieldDecl> {
93 let mut out = Vec::new();
94 for f in &self.fields {
95 collect_leaves(f, &mut out);
96 }
97 out
98 }
99
100 pub fn canonical_bytes(&self) -> Vec<u8> {
104 let mut out = Vec::new();
105 out.extend_from_slice(b"COPYBOOK\x1f");
106 out.extend_from_slice(self.record_name.as_bytes());
107 out.push(0x1f);
108 out.extend_from_slice(self.encoding.as_bytes());
109 out.push(0x1e);
110 for f in &self.fields {
111 canon_field(f, &mut out);
112 }
113 out
114 }
115}
116
117fn collect_leaves<'a>(f: &'a FieldDecl, out: &mut Vec<&'a FieldDecl>) {
118 match &f.kind {
119 FieldKind::Group(children) => {
120 for c in children {
121 collect_leaves(c, out);
122 }
123 }
124 _ => out.push(f),
125 }
126}
127
128fn canon_field(f: &FieldDecl, out: &mut Vec<u8>) {
129 out.extend_from_slice(f.name.as_bytes());
130 out.push(0x1f);
131 out.extend_from_slice(f.pic.as_bytes());
132 out.push(0x1f);
133 out.extend_from_slice(f.offset.to_string().as_bytes());
134 out.push(0x1f);
135 out.extend_from_slice(f.length.to_string().as_bytes());
136 out.push(0x1f);
137 match &f.kind {
138 FieldKind::Alphanumeric => out.extend_from_slice(b"A"),
139 FieldKind::Numeric { scale, signed } => {
140 out.extend_from_slice(b"N");
141 out.extend_from_slice(scale.to_string().as_bytes());
142 out.push(if *signed { b'S' } else { b'U' });
143 }
144 FieldKind::Group(children) => {
145 out.extend_from_slice(b"G{");
146 for c in children {
147 canon_field(c, out);
148 }
149 out.push(b'}');
150 }
151 }
152 out.push(0x1e);
153}
154
155#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct Finding {
158 pub code: String,
160 pub message: String,
162}
163
164impl Finding {
165 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
167 Finding { code: code.into(), message: message.into() }
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
174pub struct DecodedField {
175 pub name: String,
177 pub value: String,
179 pub raw: Vec<u8>,
181 pub decl: FieldDecl,
183 pub findings: Vec<Finding>,
185}
186
187#[cfg(test)]
188mod tests {
189 use super::*;
190
191 #[test]
192 fn record_length_sums_children() {
193 let cb = Copybook {
194 record_name: "REC".into(),
195 encoding: "ascii".into(),
196 fields: vec![
197 FieldDecl::alnum("NAME", "X(4)", 0, 4),
198 FieldDecl::numeric("AMT", "9(3)V99", 4, 5, 2, false),
199 ],
200 };
201 assert_eq!(cb.record_length(), 9);
202 }
203
204 #[test]
205 fn group_length_is_sum() {
206 let g = FieldDecl::group(
207 "G",
208 0,
209 vec![FieldDecl::alnum("A", "X(2)", 0, 2), FieldDecl::alnum("B", "X(3)", 2, 3)],
210 );
211 assert_eq!(g.length, 5);
212 }
213
214 #[test]
215 fn leaf_fields_flattens_groups() {
216 let cb = Copybook {
217 record_name: "REC".into(),
218 encoding: "ascii".into(),
219 fields: vec![
220 FieldDecl::alnum("ID", "X(2)", 0, 2),
221 FieldDecl::group(
222 "G",
223 2,
224 vec![FieldDecl::alnum("A", "X(2)", 2, 2), FieldDecl::alnum("B", "X(3)", 4, 3)],
225 ),
226 ],
227 };
228 let leaves: Vec<&str> = cb.leaf_fields().iter().map(|f| f.name.as_str()).collect();
229 assert_eq!(leaves, vec!["ID", "A", "B"]);
230 }
231
232 #[test]
233 fn canonical_bytes_deterministic() {
234 let cb = Copybook {
235 record_name: "REC".into(),
236 encoding: "ascii".into(),
237 fields: vec![FieldDecl::alnum("NAME", "X(4)", 0, 4)],
238 };
239 assert_eq!(cb.canonical_bytes(), cb.canonical_bytes());
240 assert!(!cb.canonical_bytes().is_empty());
241 }
242}