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
|
#!/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'
def flatten(data):
out = []
for key, value in data.items():
# this item is a requirement
if key == KEY.DESCRIPTION:
out.append(data)
# skip over reserved keys
if key in KEY: continue
# recursively flatten other requirements
items = flatten(value)
# and prefix them with the current key
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],
'',
'',
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 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])
|