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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
|
#!/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'
class REQ_TYPE(StrEnum):
SYSTEM = 'system'
USER = 'user'
class REQ_PRIORITY(StrEnum):
MUST = 'must'
SHOULD = 'should'
COULD = 'could'
WONT = '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 req: req[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])
|