]> git.ipfire.org Git - thirdparty/suricata.git/commitdiff
evedoc.py: script to generate rst doc from eve schema
authorJason Ish <jason.ish@oisf.net>
Mon, 5 Jun 2023 19:55:54 +0000 (13:55 -0600)
committerVictor Julien <victor@inliniac.net>
Mon, 7 Oct 2024 20:28:04 +0000 (22:28 +0200)
Also supports a "--flat" command line option to produce a "dot"
separated version of all the fields in the EVE schema.

scripts/evedoc.py [new file with mode: 0755]

diff --git a/scripts/evedoc.py b/scripts/evedoc.py
new file mode 100755 (executable)
index 0000000..ad23356
--- /dev/null
@@ -0,0 +1,211 @@
+#! /usr/bin/env python3
+#
+# Generate Sphinx documentation from JSON schema
+
+import argparse
+import sys
+import json
+
+
+def errprint(*args, **kwargs):
+    print(*args, file=sys.stderr, **kwargs)
+
+
+def find_ref(schema: dict, ref: str) -> dict:
+    parts = ref.split("/")
+
+    root = parts.pop(0)
+    if root != "#":
+        raise Exception("Unsupported reference: {}".format(ref))
+
+    while parts:
+        schema = schema[parts.pop(0)]
+
+    return schema
+
+
+def get_type(props: dict, name: str) -> str:
+    prop_type = props["type"]
+    if prop_type == "array":
+        try:
+            array_type = props["items"]["type"]
+        except KeyError:
+            errprint("warning: array property without items: {}".format(name))
+            array_type = "unknown"
+        prop_type = "array of {}s".format(array_type)
+    return prop_type
+
+
+def render_flat(schema: dict):
+    stack = [(schema, [])]
+
+    while stack:
+        (current, path) = stack.pop(0)
+
+        for name, props in current["properties"].items():
+            if "$ref" in props:
+                ref = find_ref(schema, props["$ref"])
+                if not ref:
+                    raise Exception("$ref not found: {}".format(props["$ref"]))
+                props = ref
+            if props["type"] in ["string", "integer", "boolean", "number"]:
+                # End of the line...
+                print("{}: {}".format(".".join(path + [name]), props["type"]))
+            elif props["type"] == "object":
+                print("{}: object".format(".".join(path + [name])))
+                if "properties" in props:
+                    stack.insert(0, (props, path + [name]))
+                else:
+                    errprint(
+                        "warning: object without properties: {}".format(
+                            ".".join(path + [name])
+                        )
+                    )
+            elif props["type"] == "array":
+                if "items" in props and "type" in props["items"]:
+                    print(
+                        "{}: {}[]".format(
+                            ".".join(path + [name]), props["items"]["type"]
+                        )
+                    )
+                    if "properties" in props["items"]:
+                        stack.insert(
+                            0,
+                            (
+                                props["items"],
+                                path + ["{}[]".format(name)],
+                            ),
+                        )
+                else:
+                    errprint(
+                        "warning: undocumented array: {}".format(
+                            ".".join(path + [name])
+                        )
+                    )
+                    print("{}: array".format(".".join(path + [name])))
+            else:
+                raise Exception("Unsupported type: {}".format(props["type"]))
+
+
+def render_rst(schema: dict):
+    stack = [(schema, [], "object")]
+
+    while stack:
+        (current, path, type) = stack.pop(0)
+
+        items = []
+
+        for name, props in current["properties"].items():
+            if "$ref" in props:
+                ref = find_ref(schema, props["$ref"])
+                if not ref:
+                    raise Exception(
+                        "Reference not found: {}".format(props["$ref"])
+                    )
+                props = ref
+            prop_type = get_type(props, name)
+            description = props.get("description", "")
+
+            items.append(
+                {"name": name, "type": prop_type, "description": description}
+            )
+
+            if props["type"] == "object" and "properties" in props:
+                stack.insert(0, (props, path + [name], "object"))
+            elif (
+                props["type"] == "array"
+                and "items" in props
+                and "properties" in props["items"]
+            ):
+                array_type = props["items"]["type"]
+                stack.insert(
+                    0,
+                    (
+                        props["items"],
+                        path + ["{}".format(name)],
+                        "array of {}s".format(array_type),
+                    ),
+                )
+
+        render_rst_table(items, path, type)
+
+
+def render_rst_table(items: list, path: list, type: str):
+    if not path:
+        title = "Top Level"
+    else:
+        title = ".".join(path)
+    title = "{} ({})".format(title, type)
+    print(title)
+    print("^" * len(title))
+
+    name_len = max([len(item["name"]) for item in items] + [len("Name")])
+    desc_len = max(
+        [len(item["description"]) for item in items] + [len("Description")]
+    )
+    type_len = max([len(item["type"]) for item in items])
+
+    print(".. table::")
+    print("   :width: 100%")
+    print("   :widths: 30 25 45")
+    print("")
+
+    print("   {} {} {}".format("=" * name_len, "=" * type_len, "=" * desc_len))
+    print(
+        "   {} {} {}".format(
+            "Name".ljust(name_len),
+            "Type".ljust(type_len),
+            "Description".ljust(desc_len),
+        )
+    )
+    print("   {} {} {}".format("=" * name_len, "=" * type_len, "=" * desc_len))
+    for item in items:
+        print(
+            "   {} {} {}".format(
+                item["name"].ljust(name_len),
+                item["type"].ljust(type_len),
+                item["description"].ljust(desc_len),
+            )
+        )
+    print("   {} {} {}".format("=" * name_len, "=" * type_len, "=" * desc_len))
+    print("")
+
+
+epilog = """
+
+By default, the EVE schema is rendered as Sphinx documentation. To
+create "flat" or "dot" separated output, use the --flat option.
+
+"""
+
+
+def main():
+    parser = argparse.ArgumentParser(
+        description="Generate documentation from JSON schema",
+        epilog=epilog,
+        formatter_class=argparse.RawTextHelpFormatter,
+    )
+    parser.add_argument("--object", help="Object name")
+    parser.add_argument("--output", help="Output file")
+    parser.add_argument("--flat", help="Flatten output", action="store_true")
+    parser.add_argument("filename", help="JSON schema file")
+
+    args = parser.parse_args()
+
+    root = json.load(open(args.filename))
+    schema = root
+
+    if args.object:
+        schema = schema["properties"][args.object]
+
+    if args.output:
+        sys.stdout = open(args.output, "w")
+
+    if args.flat:
+        render_flat(schema)
+    else:
+        render_rst(schema)
+
+
+if __name__ == "__main__":
+    sys.exit(main())