1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
|
#!/bin/python3
import sys, tomllib, tex
from enum import StrEnum
def label2ref(*labels):
return ",".join(["req:" + label.replace('.', ':') for label in labels])
class KEY(StrEnum):
LABEL = 'label'
TYPE = 'type'
ID = 'id'
INDEX = 'index'
DELETED = 'deleted'
DONE = 'done'
DESCRIPTION = 'description'
PRIORITY = 'priority'
class REQ_TYPE(StrEnum):
SYSTEM = 'system'
USER = 'user'
class REQ_PRIORITY(StrEnum):
MUST = 'must'
SHOULD = 'should'
COULD = 'could'
WONT = 'will not'
# this doesn't work right
def flatten(data):
out = []
# this key is a requirement
if KEY.DESCRIPTION in data:
out.append(data)
# check for children
for key, value in data.items():
# skip over reserved keys
if key in KEY: continue
items = flatten(value)
for item in items:
if KEY.LABEL in item:
item[KEY.LABEL] = f"{key}.{item[KEY.LABEL]}"
else:
item[KEY.LABEL] = f"{key}"
out += items
return out
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(data):
reqs = flatten(data)
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]
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], item[KEY.ID], 'ggg', 'hhh', 'iii')),
tex.cmd('newlabel', f"{ref}@cref", tex.group(f"[requirement][aaa][bbb]{item[KEY.ID]}", '[ccc][ddd][eee]fff')),
]
return "\n".join(out)
def fmt_tex(data):
out = []
for item in data:
out.append(
tex.cmd('subsection', item[KEY.ID]) +\
tex.cmd('label', label2ref(item[KEY.LABEL])) +\
tex.cmd('par') +\
tex.env('description',
tex.cmd('item', ['Priority']) + item[KEY.PRIORITY].title() +\
tex.cmd('item', ['Requirement']) + item[KEY.DESCRIPTION] +\
(tex.cmd('item', ['Definition of done']) + item[KEY.DONE] if item[KEY.DONE] is not None else "")
)
)
return "".join(out)
def main(input_file):
data = {}
with open(input_file, "rb") as file:
data = tomllib.load(file)
requirements = convert(data)
output_aux = input_file.removesuffix(".toml") + ".aux"
with open(output_aux, "w+") as file:
file.write(fmt_aux(requirements))
output_tex = input_file.removesuffix(".toml") + ".tex"
with open(output_tex, "w+") as file:
file.write(fmt_tex(requirements))
if __name__ == "__main__":
if len(sys.argv) != 2:
print("usage: reqs2tex.py reqs.toml")
exit(1)
main(sys.argv[1])
|