-
Notifications
You must be signed in to change notification settings - Fork 60
/
generate_docs.py
executable file
·167 lines (139 loc) · 5.31 KB
/
generate_docs.py
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
#!/usr/bin/env python3
# Copyright 2022-2024 Axis Communications AB.
# For a full list of individual contributors, please see the commit history.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Reads an event definition file and transforms it into a
documentation Markdown file.
Example:
./generate_docs.py definitions/EiffelCompositionDefinedEvent/3.2.0.yml
"""
import dataclasses
import os.path
import sys
from pathlib import Path
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from typing import Set
import jinja2
import definition_loader
_OUTPUT_ROOT_PATH = Path("eiffel-vocabulary")
# Optional list of field names that should be omitted from the documentation.
_SKIP_DOC_FIELDS = {"data.customData"}
@dataclasses.dataclass
class Member:
description: str
format: str
legal_values: List[Any]
name: str
typ: str
required: bool
def _filter_event_link(input: str) -> str:
"""Jinja2 filter that transforms an event type name to a links to
the documentation of that event.
"""
if input.startswith("Eiffel") and input.endswith("Event"):
return f"[{input}](../eiffel-vocabulary/{input}.md)"
return input
def _filter_member_heading(input: str) -> str:
"""Jinja2 filter that transforms an event member name to a section
heading with an appropriate depth.
"""
return (2 + input.count(".")) * "#" + " " + input
def _filter_yes_or_no(input: bool) -> str:
"""Jinja2 filter that tranforms a boolean value into either "Yes" or "No"."""
return "Yes" if input else "No"
def _get_field_enum_values(field: Dict) -> Optional[List[Any]]:
"""Returns a list of valid enum values if the given property is an
enum, otherwise None.
"""
try:
return field["enum"]
except KeyError:
try:
return field["items"]["enum"]
except KeyError:
return None
def _get_field_type(field: Dict) -> str:
"""Returns the type of a field given its property definition. Scalar
types are simply titlecased and array properties are expressed as
"InnerType[]" where InnerType is the type of each item.
"""
if "type" not in field:
return "Any"
if field["type"] == "array":
return field["items"].get("type", "object").title() + "[]"
return field["type"].title()
def _get_members(
field_prefix: str, definitions: Dict, skip: Set[str]
) -> Dict[str, Member]:
"""Returns a dict of Member objects, keyed by the full field name
(e.g. data.name or meta.source.name). Fields whose full name is
included in the skip set will be omitted from the result.
"""
result = {}
required = definitions.get("required", [])
for prop, prop_def in definitions.get("properties", {}).items():
if "$ref" in prop_def:
continue
full_name = field_prefix + prop
if full_name in skip:
continue
result[full_name] = Member(
description=prop_def.get("_description", ""),
format=prop_def.get("_format"),
legal_values=_get_field_enum_values(prop_def),
name=full_name,
typ=_get_field_type(prop_def),
required=prop in required,
)
if prop_def.get("type") == "object":
result.update(_get_members(field_prefix + prop + ".", prop_def, skip))
elif prop_def.get("type") == "array":
result.update(
_get_members(field_prefix + prop + ".", prop_def["items"], skip)
)
return result
def _main():
env = jinja2.Environment(loader=jinja2.FileSystemLoader("."))
env.filters["event_link"] = _filter_event_link
env.filters["member_heading"] = _filter_member_heading
env.filters["yes_or_no"] = _filter_yes_or_no
templ = env.get_template("event_docs.md.j2")
for filename in sys.argv[1:]:
print(filename)
input_path = Path(filename)
schema = definition_loader.load(input_path)
output_path = _OUTPUT_ROOT_PATH / (schema["_name"] + ".md")
context = {
"type": schema["_name"],
"version": schema["_version"],
"description": schema.get("_description", ""),
"abbrev": schema.get("_abbrev", ""),
"links": schema.get("_links", {}),
"data_members": _get_members(
"data.", schema["properties"]["data"], _SKIP_DOC_FIELDS
),
"meta_members": _get_members(
"meta.", schema["properties"]["meta"], _SKIP_DOC_FIELDS
),
"examples": schema.get("_examples"),
"history": schema.get("_history"),
"source_file": os.path.relpath(input_path, output_path.parent),
}
with output_path.open(mode="w") as output_file:
output_file.write(templ.render(**context))
if __name__ == "__main__":
_main()