Files
chatmail-core/scripts/generate_openrpc_bindings.py
2023-10-09 16:05:52 +00:00

178 lines
5.8 KiB
Python
Executable File

#!/usr/bin/env python3
import subprocess
import json
from pprint import pprint
def from_camel_case(name):
"""Convert a camelCase identifier to snake case."""
l = len(name)
name += "X"
res = ""
start = 0
for i in range(len(name)):
if i > 0 and name[i].isupper():
res += name[start:i].lower()
if i != l:
res += "_"
start = i
return res
def generate_method(method):
assert method["paramStructure"] == "by-position"
name = method["name"]
params = method["params"]
args_typed = ", ".join(
[
from_camel_case(param["name"])
+ ": "
+ (decode_type(param["schema"]) or "Any")
for param in params
]
)
args = ", ".join([from_camel_case(param["name"]) for param in params])
result_type = decode_type(method["result"]["schema"])
print(f"def {name}({args_typed}) -> {result_type}:")
if "description" in method:
description = method["description"]
if "\n" in description:
print(f' """{method["description"].lstrip()}\n """')
else:
print(f' """{method["description"].lstrip()}"""')
print(f" rpc_call({args})")
print()
def generate_openrpc_methods(openrpc_spec):
for method in openrpc_spec["methods"]:
generate_method(method)
def decode_type(property_desc):
if "anyOf" in property_desc:
t = property_desc["anyOf"]
assert len(t) == 2
assert t[1] == {"type": "null"}
ref = t[0]["$ref"]
assert ref.startswith("#/components/schemas/")
return f'Optional["{ref.removeprefix("#/components/schemas/")}"]'
elif "$ref" in property_desc:
t = property_desc["$ref"]
assert t.startswith("#/components/schemas/")
t = t.removeprefix("#/components/schemas/")
return f'"{t}"'
elif property_desc["type"] == "null":
return "None"
elif "null" in property_desc["type"]:
assert len(property_desc["type"]) == 2
assert property_desc["type"][1] == "null"
property_desc["type"] = property_desc["type"][0]
t = decode_type(property_desc)
if t:
return f"Optional[{t}]"
elif property_desc["type"] == "boolean":
return "bool"
elif property_desc["type"] == "integer":
return "int"
elif property_desc["type"] == "number" and property_desc["format"] == "double":
return "float"
elif property_desc["type"] == "string":
return "str"
elif property_desc["type"] == "array":
if isinstance(property_desc["items"], list):
items_desc = ", ".join(decode_type(x) for x in property_desc["items"])
return f"Tuple[{items_desc}]"
else:
items_type = decode_type(property_desc["items"])
return f"list[{items_type}]"
elif "additionalProperties" in property_desc:
additional_properties = property_desc["additionalProperties"]
return f"dict[Any, {decode_type(additional_properties)}]"
return None
def generate_variant(variant) -> str:
"""Prints generated type for enum variant.
Returns the name of the generated type.
"""
assert variant["type"] == "object"
kind = variant["properties"]["kind"]
assert kind["type"] == "string"
assert len(kind["enum"]) == 1
kind_name = kind["enum"][0]
kind_name = kind_name[0].upper() + kind_name[1:]
print(f" @dataclass(kw_only=True)")
print(f" class {kind_name}:")
print(f" kind: str = \"{kind_name}\"")
for property_name, property_desc in variant["properties"].items():
property_name = from_camel_case(property_name)
if property_name == "kind":
continue
if t := decode_type(property_desc):
print(f" {property_name}: {t}")
else:
print("# TODO")
pprint(property_name)
pprint(property_desc)
print()
return kind_name
def generate_type(type_name, schema):
if "oneOf" in schema:
if all(x["type"] == "string" for x in schema["oneOf"]):
# Simple enumeration consisting only of various string types.
print(f"class {type_name}(Enum):")
for x in schema["oneOf"]:
for e in x["enum"]:
print(f' {from_camel_case(e).upper()} = "{e}"')
else:
# Union type.
namespace = f"{type_name}Enum"
print(f"class {namespace}:")
kind_names = [f"{namespace}.{generate_variant(x)}" for x in schema["oneOf"]]
print(f"{type_name}: TypeAlias = {' | '.join(kind_names)}")
elif schema["type"] == "string":
print(f"class {type_name}(Enum):")
for e in schema["enum"]:
print(f' {from_camel_case(e).upper()} = "{e}"')
else:
print("@dataclass(kw_only=True)")
print(f"class {type_name}:")
for property_name, property_desc in schema["properties"].items():
property_name = from_camel_case(property_name)
if decode_type(property_desc):
print(f" {property_name}: {decode_type(property_desc)}")
else:
print(f"# TODO {property_name}")
pprint(property_desc)
print()
def generate_openrpc_types(openrpc_spec):
for type_name, schema in openrpc_spec["components"]["schemas"].items():
generate_type(type_name, schema)
def main():
openrpc_spec = json.loads(
subprocess.run(
["deltachat-rpc-server", "--openrpc"], capture_output=True
).stdout
)
print("from dataclasses import dataclass")
print("from enum import Enum")
print("from typing import TypeAlias, Union, Optional, Tuple, Any")
generate_openrpc_types(openrpc_spec)
generate_openrpc_methods(openrpc_spec)
if __name__ == "__main__":
main()