1use std::sync::OnceLock;
6
7const COMMANDS: &str = include_str!("../data/cics-commands.tsv");
8const DFHRESP: &str = include_str!("../data/dfhresp.tsv");
9const DFHVALUE: &str = include_str!("../data/dfhvalue.tsv");
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub enum Direction {
13 Sends,
14 Receives,
15 Both,
16 Label,
18 None,
19}
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct CommandOption {
23 pub name: &'static str,
25 pub argument: &'static str,
27 pub direction: Direction,
28}
29
30#[derive(Clone, Debug, PartialEq, Eq)]
31pub struct Command {
32 pub name: &'static str,
33 pub identify: Vec<&'static str>,
35 pub default: bool,
37 pub options: Vec<CommandOption>,
38 pub doc: &'static str,
40 pub page: Option<u32>,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
45pub struct Resp {
46 pub condition: &'static str,
47 pub value: i32,
48 pub source: &'static str,
50 pub page: Option<u32>,
51}
52
53struct Tables {
54 commands: Vec<Command>,
55 every: Vec<CommandOption>,
56 resp: Vec<Resp>,
57 cvda: Vec<(&'static str, i32)>,
58}
59
60fn rows(text: &'static str) -> impl Iterator<Item = Vec<&'static str>> {
61 text.lines().filter(|l| !l.is_empty() && !l.starts_with('#')).map(|l| l.split('\t').collect())
62}
63
64fn direction(word: &str) -> Direction {
65 match word {
66 "sends" => Direction::Sends,
67 "receives" => Direction::Receives,
68 "both" => Direction::Both,
69 "label" => Direction::Label,
70 "-" => Direction::None,
71 other => panic!("cics-commands.tsv: no direction {other}"),
72 }
73}
74
75fn number(cell: &str, table: &str) -> i32 {
76 cell.parse().unwrap_or_else(|_| panic!("{table}: {cell} is not a number"))
77}
78
79fn tables() -> &'static Tables {
80 static TABLES: OnceLock<Tables> = OnceLock::new();
81 TABLES.get_or_init(|| {
82 let (mut commands, mut every) = (Vec::<Command>::new(), Vec::new());
83 for row in rows(COMMANDS) {
84 let [command, identify, default, option, argument, dir, doc, page] = row[..] else { panic!("cics-commands.tsv: {row:?} is not eight cells") };
85 let option = (option != "-").then(|| CommandOption { name: option, argument, direction: direction(dir) });
86 if command == "*" {
87 every.extend(option);
88 continue;
89 }
90 if commands.last().is_none_or(|c| c.name != command) {
91 let identify = if identify == "-" { Vec::new() } else { identify.split(' ').collect() };
92 commands.push(Command { name: command, identify, default: default == "yes", options: Vec::new(), doc, page: page.parse().ok() });
93 }
94 if let Some(c) = commands.last_mut() {
95 c.options.extend(option);
96 }
97 }
98 let resp = rows(DFHRESP)
99 .map(|row| {
100 let [condition, value, source, page] = row[..] else { panic!("dfhresp.tsv: {row:?} is not four cells") };
101 Resp { condition, value: number(value, "dfhresp.tsv"), source, page: page.parse().ok() }
102 })
103 .collect();
104 let cvda = rows(DFHVALUE)
105 .map(|row| {
106 let [name, value, _since, _source] = row[..] else { panic!("dfhvalue.tsv: {row:?} is not four cells") };
107 (name, number(value, "dfhvalue.tsv"))
108 })
109 .collect();
110 Tables { commands, every, resp, cvda }
111 })
112}
113
114pub fn commands() -> &'static [Command] {
116 &tables().commands
117}
118
119pub fn command(name: &str) -> Option<&'static Command> {
120 commands().iter().find(|c| c.name.eq_ignore_ascii_case(name))
121}
122
123pub fn every_command_options() -> &'static [CommandOption] {
125 &tables().every
126}
127
128pub fn resp_table() -> &'static [Resp] {
130 &tables().resp
131}
132
133pub fn resp(condition: &str) -> Option<i32> {
135 resp_table().iter().find(|r| r.condition.eq_ignore_ascii_case(condition)).map(|r| r.value)
136}
137
138pub fn resp_condition(value: i32) -> Option<&'static str> {
139 resp_table().iter().find(|r| r.value == value).map(|r| r.condition)
140}
141
142pub fn cvda(name: &str) -> Option<i32> {
144 tables().cvda.iter().find(|(n, _)| n.eq_ignore_ascii_case(name)).map(|&(_, v)| v)
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn the_tables_hold_every_row() {
153 assert_eq!((commands().len(), resp_table().len(), tables().cvda.len()), (67, 121, 1061));
154 assert_eq!(every_command_options().iter().map(|o| o.name).collect::<Vec<_>>(), ["RESP", "RESP2"]);
155 assert!(commands().iter().all(|c| c.doc.starts_with("https://www.ibm.com/docs/") && c.page.is_some()), "every command cites IBM");
156 }
157
158 #[test]
159 fn conditions_and_cvdas_have_ibms_numbers() {
160 let got: Vec<_> = ["NORMAL", "NOTFND", "ROLLEDBACK", "NOTFINISHED", "BUSY"].iter().map(|c| resp(c)).collect();
161 assert_eq!(got, [Some(0), Some(13), Some(82), Some(113), Some(128)]);
162 assert_eq!((resp("notfnd"), resp("NOSUCH"), resp_condition(82)), (Some(13), None, Some("ROLLEDBACK")));
163 let busy = resp_table().last().unwrap();
164 assert_eq!((busy.condition, busy.page), ("BUSY", None));
165 assert_eq!((cvda("ABEND"), cvda("ALLOCATD"), cvda("ADDRESS")), (Some(900), Some(81), Some(859)), "ADDRESS as CICS TS 5.4 and later give it");
166 assert_eq!((cvda("SECERROR"), cvda("NODEJSAPP"), cvda("AWARE"), cvda("NOTAWARE")), (Some(1214), Some(1215), Some(1256), Some(1257)), "5.4, 5.5 and 6.x values; AWARE from 6.x's numeric table");
167 }
168
169 #[test]
170 fn commands_carry_their_options_and_what_tells_them_apart() {
171 let send_map = command("send map").unwrap();
172 assert_eq!((send_map.identify.as_slice(), send_map.page), (&["MAP"][..], Some(605)));
173 let option = |c: &Command, name: &str| *c.options.iter().find(|o| o.name == name).unwrap();
174 assert_eq!(option(send_map, "FROM"), CommandOption { name: "FROM", argument: "data-area", direction: Direction::Sends });
175 assert_eq!(option(send_map, "SET").direction, Direction::Receives);
176 assert_eq!(command("HANDLE CONDITION").unwrap().options, [CommandOption { name: "*", argument: "label", direction: Direction::Label }]);
177 assert_eq!(command("IGNORE CONDITION").unwrap().options[0].direction, Direction::None);
178 assert!(command("SYNCPOINT").unwrap().options.is_empty());
179 assert!(command("READQ TS").unwrap().default && !command("READQ TD").unwrap().default);
180 }
181
182 #[test]
184 fn the_vendored_tables_are_cobolworks() {
185 let Ok(dir) = std::env::var("IRONWORK_COBOLWORK_DIR") else { return };
186 for (table, ours) in [("cics-commands", COMMANDS), ("dfhresp", DFHRESP), ("dfhvalue", DFHVALUE)] {
187 let theirs = std::fs::read_to_string(std::path::Path::new(&dir).join(format!("provenance/{table}.tsv"))).expect("cobolwork's table");
188 assert!(theirs.replace('\r', "") == ours.replace('\r', ""), "crates/rt/data/{table}.tsv differs from cobolwork's: run tools/sync-cics-tables.sh");
189 }
190 }
191}