aboutsummaryrefslogtreecommitdiff
path: root/scripts/reqs2tex.py
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/reqs2tex.py')
-rwxr-xr-xscripts/reqs2tex.py173
1 files changed, 173 insertions, 0 deletions
diff --git a/scripts/reqs2tex.py b/scripts/reqs2tex.py
new file mode 100755
index 0000000..2bab558
--- /dev/null
+++ b/scripts/reqs2tex.py
@@ -0,0 +1,173 @@
+#!/bin/python3
+import sys, tomllib, tex, re
+from enum import StrEnum
+
+def label2ref(*labels):
+ return ",".join(["req:" + label for label in labels])
+
+class KEY(StrEnum):
+ LABEL = 'label'
+ TYPE = 'type'
+ ID = 'id'
+ INDEX = 'index'
+ DELETED = 'deleted'
+ DONE = 'done'
+ DESCRIPTION = 'description'
+ PRIORITY = 'priority'
+
+REQ_TYPE = [
+ 'system',
+ 'user',
+]
+
+REQ_PRIORITY = [
+ 'must',
+ 'should',
+ 'could',
+ 'will not',
+]
+
+id_counter = 0
+def make_id(item):
+ global id_counter
+ id_counter += 1
+ return "{type_short}#{counter:03d}".format(
+ type_short = item[KEY.TYPE][0].upper(),
+ counter = id_counter,
+ )
+
+def sanitize(item, ids):
+ def die(msg):
+ print(f"[{item[KEY.LABEL]}]: {msg}")
+ exit(1)
+
+ # ensure properties
+ item[KEY.DESCRIPTION] = item.get(KEY.DESCRIPTION)
+ item[KEY.DONE] = item.get(KEY.DONE)
+ item[KEY.PRIORITY] = item.get(KEY.PRIORITY)
+ item[KEY.TYPE] = item.get(KEY.TYPE)
+
+ # type checks
+ if item[KEY.TYPE] not in REQ_TYPE:
+ die(f"unknown or missing requirement type: {repr(item[KEY.TYPE])}")
+ if item[KEY.PRIORITY] not in REQ_PRIORITY:
+ die(f"unknown or missing requirement priority: {repr(item[KEY.PRIORITY])}")
+
+ # conversions
+ if isinstance(item[KEY.DONE], list):
+ # safety check
+ if not set(item[KEY.DONE]).issubset(ids):
+ die("definition of done includes unknown requirement(s)")
+ item[KEY.DONE] = tex.cmd('Cref', label2ref(*item[KEY.DONE]))
+
+def convert(reqs):
+ all_ids = [item[KEY.LABEL] for item in reqs]
+ index = 0
+ for item in reqs:
+ item[KEY.ID] = tex.esc(make_id(item))
+ item[KEY.DELETED] = item.get(KEY.DELETED, False)
+ if item[KEY.DELETED]: continue
+ item[KEY.INDEX] = index
+ index += 1
+ sanitize(item, all_ids)
+
+ # skip deleted requirements (but process for make_id)
+ reqs = [item for item in reqs if item[KEY.DELETED] == False]
+
+ # sort by label
+ reqs = sorted(reqs, key=lambda item: item[KEY.LABEL])
+
+ return reqs
+
+def fmt_aux(data):
+ out = []
+ for item in data:
+ ref = label2ref(item[KEY.LABEL])
+ out += [
+ tex.cmd('newlabel', f"{ref}", tex.group(
+ item[KEY.ID],
+ '',
+ '',
+ ref,
+ '',
+ )),
+ tex.cmd('newlabel', f"{ref}@cref", tex.group(
+ f"[requirement][][]{item[KEY.ID]}",
+ '[][][]',
+ '',
+ '',
+ '',
+ )),
+ ]
+ return "\n".join(out)
+
+def fmt_tex(data):
+ out = ""
+ for item in data:
+ out += tex.join(
+ tex.cmd('subsection', f"{item[KEY.ID]}: {item[KEY.LABEL]}".upper()),
+ tex.withatletter(
+ tex.cmd('cref@constructprefix', 'requirement', r'\cref@result'),
+ tex.pedef('@currentlabel', item[KEY.ID]),
+ tex.pedef('@currentlabelname', item[KEY.ID]),
+ tex.pedef('cref@currentlabel', tex.group(['requirement'], [''], [r'\cref@result']) + item[KEY.ID]),
+ ),
+ tex.cmd('label', ['requirement'], label2ref(item[KEY.LABEL])),
+ tex.cmd('parbox', tex.cmd('linewidth'),
+ tex.env('description', tex.join(
+ tex.cmd('item', [tex.cmd('reqlabel', 'priority')]),
+ item[KEY.PRIORITY].title(),
+ tex.cmd('item', [tex.cmd('reqlabel', 'description')]),
+ item[KEY.DESCRIPTION],
+ *([
+ tex.cmd('item', [tex.cmd('reqlabel', 'done')]),
+ item[KEY.DONE]
+ ] if item[KEY.DONE] is not None else []),
+ )),
+ )
+ )
+ return out
+
+def tomlload(content):
+ # replace requirement labels with temp value
+ label_map = dict()
+ label_idx = 0
+ lines = content.split("\n")
+ for index, line in enumerate(lines):
+ match = re.search(r"^\s*\[(.+)\]", line)
+ if match is None: continue
+ lines[index] = f"[{label_idx}]"
+ label_map[str(label_idx)] = match.group(1)
+ label_idx += 1
+ content = "\n".join(lines)
+
+ # load TOML and replace temporary labels with real labels
+ data_dict = tomllib.loads(content)
+ data_list = []
+ for key, value in data_dict.items():
+ value[KEY.LABEL] = label_map[key]
+ data_list.append(value)
+
+ return data_list
+
+def main(input_file):
+ data = []
+ with open(input_file, "r") as file:
+ data = tomlload(file.read())
+
+ items = convert(data)
+
+ output_aux = input_file.removesuffix(".toml") + ".aux"
+ with open(output_aux, "w+") as file:
+ file.write(fmt_aux(items))
+
+ output_tex = input_file.removesuffix(".toml") + ".tex"
+ with open(output_tex, "w+") as file:
+ file.write(fmt_tex(items))
+
+if __name__ == "__main__":
+ if len(sys.argv) != 2:
+ print("usage: reqs2tex.py reqs.toml")
+ exit(1)
+ main(sys.argv[1])
+