aboutsummaryrefslogtreecommitdiff
path: root/scripts/reqs2tex.py
blob: e5f063de82716c807afb188886c0d9ce9c04ab7d (plain)
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])