diff options
-rw-r--r-- | reqs.toml | 25 | ||||
-rwxr-xr-x | scripts/reqs2tex.py | 42 | ||||
-rw-r--r-- | scripts/tex.py | 3 |
3 files changed, 63 insertions, 7 deletions
@@ -1,10 +1,35 @@ +# This is a TOML file containing all project requirements. The reqs2tex script +# can be used to generate the files necessary to compile requirements.tex and +# cross-reference the requirements from other documents. +# +# Note that TOML has a hash table structure, so keys used for defining +# requirement properties cannot be used for requirement IDs. The properties are: +# label, type, id, index, deleted, done, description, priority + +# This is the requirement cross-reference ID. Requirements can be +# (cross-)referenced from LaTeX by prefixing this ID with `req:` and +# substituting dots for colons (i.e. this requirement is referenced as +# \cref{req:audio:async-api}). [audio.async-api] +# Requirement type ('system' | 'user') type = 'system' +# MoSCoW priority ('must' | 'should' | 'could' | 'will not') priority = 'must' +# Requirement body. Supports LaTeX formatting. (tip: use single quotes so +# backslash doesn't act as an escape character) description = ''' The public audio \gls{api} supports starting audio samples asynchronously (i.e.~fire and forget). ''' +# Definition of done (user requirements only). If 'done' is a string, it is +# treated as LaTeX code (like description), if it is a list of strings, each +# item is treated as the ID of another requirement. +#done = 'When I feel like it' +#done = [ 'audio.handle', 'audio.stream-mix' ] +# Requirements that are no longer applicable should set `deleted` to `true`. +# This will make sure the requirements are numbered consistently across +# different document revisions. +#deleted = true [audio.handle] type = 'system' diff --git a/scripts/reqs2tex.py b/scripts/reqs2tex.py index 9e71a48..c5ab3dd 100755 --- a/scripts/reqs2tex.py +++ b/scripts/reqs2tex.py @@ -9,7 +9,7 @@ def flatten(data): items = flatten(value) for item in items: if 'label' in item: - item['label'] = f"{key}:{item['label']}" + item['label'] = f"{key}.{item['label']}" else: item['label'] = f"{key}" out += items @@ -24,16 +24,44 @@ def make_id(item): counter = id_counter, ) +def sanitize(item, ids): + def die(msg): + print(f"[{item['label']}]: {msg}") + exit(1) + + # ensure properties + item['description'] = item.get('description') + item['done'] = item.get('done') + item['priority'] = item.get('priority') + item['type'] = item.get('type') + + # type checks + if item['type'] not in ['system', 'user']: + die(f"unknown or missing requirement type {repr(item['type'])}") + if item['priority'] not in ['must', 'should', 'could', 'will not']: + die(f"unknown or missing requirement priority {repr(item['type'])}") + + # logic checks + if item['type'] != 'user' and item['done'] is not None: + die("has definition of done but is not a user requirement") + + # conversions + if isinstance(item['done'], list): + # safety check + if not set(item['done']).issubset(ids): + die("definition of done includes unknown requirement(s)") + item['done'] = tex.cmd('Cref', tex.label2ref(*item['done'])) + def convert(data): reqs = flatten(data) - for index, item in enumerate(reqs): + index = 0 + for item in reqs: item['id'] = tex.esc(make_id(item)) - item['index'] = index - item['description'] = item.get('description', '???') - item['done'] = item.get('done', None) - item['priority'] = item.get('priority', 'must') - item['type'] = item.get('type', 'system') item['deleted'] = item.get('deleted', False) + if item['deleted']: continue + item['index'] = index + index += 1 + sanitize(item, [req['label'] for req in reqs]) # skip deleted requirements (but process for make_id) reqs = [item for item in reqs if item['deleted'] == False] diff --git a/scripts/tex.py b/scripts/tex.py index b044857..2fd51d8 100644 --- a/scripts/tex.py +++ b/scripts/tex.py @@ -41,3 +41,6 @@ def esc(plain): def tabrule(*cells): return "&".join(cells) + "\\\\" +def label2ref(*labels): + return ",".join(["req:" + label.replace('.', ':') for label in labels]) + |