Skip to content

Commit 88d0087

Browse files
committed
Add python OpenRPC generator
1 parent e12044e commit 88d0087

File tree

1 file changed

+175
-0
lines changed

1 file changed

+175
-0
lines changed

scripts/generate_openrpc_bindings.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
#!/usr/bin/env python3
2+
import subprocess
3+
import json
4+
from pprint import pprint
5+
6+
7+
def from_camel_case(name):
8+
"""Convert a camelCase identifier to snake case."""
9+
l = len(name)
10+
name += "X"
11+
res = ""
12+
start = 0
13+
for i in range(len(name)):
14+
if i > 0 and name[i].isupper():
15+
res += name[start:i].lower()
16+
if i != l:
17+
res += "_"
18+
start = i
19+
return res
20+
21+
22+
def generate_method(method):
23+
assert method["paramStructure"] == "by-position"
24+
name = method["name"]
25+
params = method["params"]
26+
args_typed = ", ".join(
27+
[
28+
from_camel_case(param["name"])
29+
+ ": "
30+
+ (decode_type(param["schema"]) or "Any")
31+
for param in params
32+
]
33+
)
34+
args = ", ".join([from_camel_case(param["name"]) for param in params])
35+
result_type = decode_type(method["result"]["schema"])
36+
print(f"def {name}({args_typed}) -> {result_type}:")
37+
if "description" in method:
38+
description = method["description"]
39+
if "\n" in description:
40+
print(f' """{method["description"].lstrip()}\n """')
41+
else:
42+
print(f' """{method["description"].lstrip()}"""')
43+
print(f" rpc_call({args})")
44+
print()
45+
46+
47+
def generate_openrpc_methods(openrpc_spec):
48+
for method in openrpc_spec["methods"]:
49+
generate_method(method)
50+
51+
52+
def decode_type(property_desc):
53+
if "anyOf" in property_desc:
54+
t = property_desc["anyOf"]
55+
assert len(t) == 2
56+
assert t[1] == {"type": "null"}
57+
ref = t[0]["$ref"]
58+
assert ref.startswith("#/components/schemas/")
59+
return f'Optional["{ref.removeprefix("#/components/schemas/")}"]'
60+
elif "$ref" in property_desc:
61+
t = property_desc["$ref"]
62+
assert t.startswith("#/components/schemas/")
63+
t = t.removeprefix("#/components/schemas/")
64+
return f'"{t}"'
65+
elif property_desc["type"] == "null":
66+
return "None"
67+
elif "null" in property_desc["type"]:
68+
assert len(property_desc["type"]) == 2
69+
assert property_desc["type"][1] == "null"
70+
property_desc["type"] = property_desc["type"][0]
71+
t = decode_type(property_desc)
72+
if t:
73+
return f"Optional[{t}]"
74+
elif property_desc["type"] == "boolean":
75+
return "bool"
76+
elif property_desc["type"] == "integer":
77+
return "int"
78+
elif property_desc["type"] == "number" and property_desc["format"] == "double":
79+
return "float"
80+
elif property_desc["type"] == "string":
81+
return "str"
82+
elif property_desc["type"] == "array":
83+
if isinstance(property_desc["items"], list):
84+
items_desc = ", ".join(decode_type(x) for x in property_desc["items"])
85+
return f"Tuple[{items_desc}]"
86+
else:
87+
items_type = decode_type(property_desc["items"])
88+
return f"list[{items_type}]"
89+
else:
90+
print("# TODO: " + str(property_desc))
91+
return None
92+
93+
94+
def generate_variant(variant) -> str:
95+
"""Prints generated type for enum variant.
96+
97+
Returns the name of the generated type.
98+
"""
99+
assert variant["type"] == "object"
100+
kind = variant["properties"]["kind"]
101+
assert kind["type"] == "string"
102+
assert len(kind["enum"]) == 1
103+
kind_name = kind["enum"][0]
104+
kind_name = kind_name[0].upper() + kind_name[1:]
105+
106+
print(f" @dataclass")
107+
print(f" class {kind_name}:")
108+
for property_name, property_desc in variant["properties"].items():
109+
property_name = from_camel_case(property_name)
110+
if t := decode_type(property_desc):
111+
print(f" {property_name}: {t}")
112+
else:
113+
print("# TODO")
114+
pprint(property_name)
115+
pprint(property_desc)
116+
print()
117+
118+
# pprint(variant)
119+
120+
return kind_name
121+
122+
123+
def generate_type(type_name, schema):
124+
if "oneOf" in schema:
125+
if all(x["type"] == "string" for x in schema["oneOf"]):
126+
# Simple enumeration consisting only of various string types.
127+
print(f"class {type_name}(Enum):")
128+
for x in schema["oneOf"]:
129+
for e in x["enum"]:
130+
print(f' {from_camel_case(e).upper()} = "{e}"')
131+
else:
132+
# Union type.
133+
namespace = f"{type_name}Enum"
134+
print(f"class {namespace}:")
135+
kind_names = [f"{namespace}.{generate_variant(x)}" for x in schema["oneOf"]]
136+
137+
print(f"{type_name}: TypeAlias = {' | '.join(kind_names)}")
138+
elif schema["type"] == "string":
139+
print(f"class {type_name}(Enum):")
140+
for e in schema["enum"]:
141+
print(f' {from_camel_case(e).upper()} = "{e}"')
142+
else:
143+
print("@dataclass")
144+
print(f"class {type_name}:")
145+
for property_name, property_desc in schema["properties"].items():
146+
property_name = from_camel_case(property_name)
147+
if decode_type(property_desc):
148+
print(f" {property_name}: {decode_type(property_desc)}")
149+
else:
150+
print(f"# TODO {property_name}")
151+
pprint(property_desc)
152+
153+
print()
154+
155+
156+
def generate_openrpc_types(openrpc_spec):
157+
for type_name, schema in openrpc_spec["components"]["schemas"].items():
158+
generate_type(type_name, schema)
159+
160+
161+
def main():
162+
openrpc_spec = json.loads(
163+
subprocess.run(
164+
["deltachat-rpc-server", "--openrpc"], capture_output=True
165+
).stdout
166+
)
167+
print("from dataclasses import dataclass")
168+
print("from enum import Enum")
169+
print("from typing import TypeAlias, Union, Optional, Tuple, Any")
170+
generate_openrpc_types(openrpc_spec)
171+
generate_openrpc_methods(openrpc_spec)
172+
173+
174+
if __name__ == "__main__":
175+
main()

0 commit comments

Comments
 (0)