Skip to main content

ironwork_rt/
cics_tables.rs

1//! IBM's CICS tables, vendored from cobolwork's provenance by tools/sync-cics-tables.sh: each
2//! command's options with the direction their data moves, the numbers DFHRESP names, and the
3//! numbers DFHVALUE names.
4
5use 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    /// A paragraph or section to branch to, as HANDLE CONDITION takes.
17    Label,
18    None,
19}
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct CommandOption {
23    /// `*` stands for any condition or AID name, as HANDLE CONDITION and IGNORE CONDITION take.
24    pub name: &'static str,
25    /// As the reference types it: data-area, data-value, ptr-ref, cvda, name and so on; `-` for none.
26    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    /// The options that tell it from a namesake, as MAP tells SEND MAP from SEND.
34    pub identify: Vec<&'static str>,
35    /// The namesake meant when none of the identify options is written, as TS is for READQ.
36    pub default: bool,
37    pub options: Vec<CommandOption>,
38    /// The command's CICS TS 6.x topic.
39    pub doc: &'static str,
40    /// Its page in the CICS TS 5.3 Application Programming Reference (SC34-7402-00).
41    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    /// The document that gives the value, and its page when it is a book.
49    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
114/// Every command cobolwork's precompiler translates, in the table's order.
115pub 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
123/// The options any command may carry: RESP and RESP2.
124pub fn every_command_options() -> &'static [CommandOption] {
125    &tables().every
126}
127
128/// Every condition DFHRESP names, by value.
129pub fn resp_table() -> &'static [Resp] {
130    &tables().resp
131}
132
133/// The number DFHRESP(condition) stands for.
134pub 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
142/// The number DFHVALUE(cvda) stands for.
143pub 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    /// Run with IRONWORK_COBOLWORK_DIR naming a cobolwork checkout to check the vendored copies.
183    #[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}