Files
MusicServer/cmd/docs/generate_godot_openapi.py
T

222 lines
8.1 KiB
Python

#!/usr/bin/env python3
import argparse
import json
import re
from pathlib import Path
from collections import defaultdict
def load_openapi_spec(path: str) -> dict:
"""Load OpenAPI spec from YAML or JSON file."""
try:
import yaml
with open(path, "r") as f:
return yaml.safe_load(f)
except ImportError:
# Fallback to JSON if PyYAML is not installed
with open(path, "r") as f:
return json.load(f)
def map_type(openapi_type: str) -> str:
"""Map OpenAPI types to GDScript types."""
type_mapping = {
"integer": "int",
"number": "float",
"boolean": "bool",
"array": "Array",
"object": "Dictionary",
}
return type_mapping.get(openapi_type, "String")
def default_value(openapi_type: str):
"""Return default values for GDScript types."""
default_mapping = {
"integer": "0",
"number": "0.0",
"boolean": "false",
"array": "[]",
"object": "{}",
}
return default_mapping.get(openapi_type, '""')
def sanitize_class_name(name: str) -> str:
"""Convert a name to a valid GDScript class name."""
# Replace invalid characters with underscores
name = re.sub(r"[^a-zA-Z0-9_]", "_", name)
# Capitalize first letter
return name[0].upper() + name[1:] if name else "Model"
def generate_model_class(class_name: str, schema: dict) -> str:
"""Generate a GDScript class for a model."""
lines = [
f"class_name {class_name}",
"extends RefCounted",
"",
]
# Add properties
properties = schema.get("properties", {})
for prop_name, prop_schema in properties.items():
prop_type = map_type(prop_schema.get("type", "string"))
lines.append(f"var {prop_name}: {prop_type}")
# Add _init method
lines.extend([
"",
"func _init(data: Dictionary):",
])
for prop_name in properties:
prop_type = map_type(properties[prop_name].get("type", "string"))
default = default_value(properties[prop_name].get("type", "string"))
lines.append(f' {prop_name} = data.get("{prop_name}", {default})')
return "\n".join(lines)
def generate_api_client(path: str, method: str, endpoint: dict) -> str:
"""Generate a GDScript API client for an endpoint."""
# Sanitize path for class name
class_name = sanitize_class_name(path.replace("/", "_").replace("{", "").replace("}", "")) + method.capitalize()
# Format URL (replace {param} with %s for Godot's string formatting)
url = path.replace("{", "%").replace("}", "s")
full_url = f'"{BASE_URL}{url}"'
lines = [
f"class_name {class_name}",
"extends RefCounted",
"",
"var http_request: HTTPRequest",
"",
"func _init(node: Node):",
" http_request = HTTPRequest.new()",
" node.add_child(http_request)",
' http_request.connect("request_completed", self, "_on_request_completed")',
"",
f"func call(params: Dictionary, callback: Callable):",
f" var url := {full_url}",
' var headers = ["User-Agent: MyGodotApp"]',
" var error := http_request.request(url, headers)",
" if error != OK:",
' push_error("HTTP request failed.")',
" return",
" http_request.set_meta(\"callback\", callback)",
"",
"func _on_request_completed(result: int, response_code: int, headers: PoolStringArray, body: PoolByteArray):",
" var callback := http_request.get_meta(\"callback\")",
" if callback:",
" var response_body = body.get_string_from_utf8()",
" var json = JSON.new()",
" if json.parse(response_body) == OK:",
" callback.call(json.get_data())",
" else:",
" callback.call(null)",
]
return "\n".join(lines)
def generate_tag_client(tag: str, endpoints: list, base_url: str) -> str:
"""Generate a GDScript API client for all endpoints with a given tag."""
class_name = sanitize_class_name(tag) + "API"
lines = [
f"class_name {class_name}",
"extends RefCounted",
"",
"var http_request: HTTPRequest",
"",
"func _init(node: Node):",
" http_request = HTTPRequest.new()",
" node.add_child(http_request)",
' http_request.connect("request_completed", self, "_on_request_completed")',
"",
]
# Generate a method for each endpoint
for path, method, endpoint in endpoints:
# Sanitize method name for GDScript
method_name = method.lower()
# Create a valid function name from the path
func_name = "call_" + path.replace("/", "_").replace("{", "").replace("}", "").replace("-", "_")
# Format URL (replace {param} with %s for Godot's string formatting)
url = path.replace("{", "%").replace("}", "s")
full_url = f'"{base_url}{url}"'
lines.extend([
f"func {func_name}(params: Dictionary = {{}}, callback: Callable):",
f" var url := {full_url}",
' var headers = ["User-Agent: MyGodotApp"]',
f" var error := http_request.request(url, headers, false, HTTPClient.METHOD_{method.upper()})",
" if error != OK:",
' push_error("HTTP request failed.")',
" return",
" http_request.set_meta(\"callback\", callback)",
"",
])
# Add the completion handler
lines.extend([
"func _on_request_completed(result: int, response_code: int, headers: PoolStringArray, body: PoolByteArray):",
" var callback := http_request.get_meta(\"callback\")",
" if callback:",
" var response_body = body.get_string_from_utf8()",
" var json = JSON.new()",
" if json.parse(response_body) == OK:",
" callback.call(json.get_data())",
" else:",
" callback.call(null)",
])
return "\n".join(lines)
def generate_code(spec: dict, output_dir: str, base_url: str):
"""Generate all GDScript files from the OpenAPI spec."""
# Create output directory
Path(output_dir).mkdir(parents=True, exist_ok=True)
# Generate models
schemas = spec.get("definitions", {}) # Swagger 2.0 uses "definitions"
if not schemas:
schemas = spec.get("components", {}).get("schemas", {})
for schema_name, schema in schemas.items():
class_name = sanitize_class_name(schema_name)
code = generate_model_class(class_name, schema)
output_path = Path(output_dir) / f"{class_name}.gd"
with open(output_path, "w") as f:
f.write(code)
print(f"Generated model: {output_path}")
# Group endpoints by tag
paths = spec.get("paths", {})
tag_endpoints = defaultdict(list)
for path, methods in paths.items():
for method, endpoint in methods.items():
tags = endpoint.get("tags", ["default"])
for tag in tags:
tag_endpoints[tag].append((path, method, endpoint))
# Generate one file per tag
for tag, endpoints in tag_endpoints.items():
code = generate_tag_client(tag, endpoints, base_url)
class_name = sanitize_class_name(tag) + "API"
output_path = Path(output_dir) / f"{class_name}.gd"
with open(output_path, "w") as f:
f.write(code)
print(f"Generated API client for tag '{tag}': {output_path}")
def main():
parser = argparse.ArgumentParser(description="Generate Godot API clients from OpenAPI spec")
parser.add_argument("input", help="Path to the OpenAPI JSON/YAML file")
parser.add_argument("-o", "--output", default="godot_generated", help="Output directory for GDScript files")
parser.add_argument("-b", "--base-url", default="https://api.example.com", help="Base URL for API requests")
args = parser.parse_args()
spec = load_openapi_spec(args.input)
generate_code(spec, args.output, args.base_url)
print("Done!")
if __name__ == "__main__":
main()