diff --git a/cmd/docs/generate_godot_openapi.py b/cmd/docs/generate_godot_openapi.py new file mode 100644 index 0000000..362abdf --- /dev/null +++ b/cmd/docs/generate_godot_openapi.py @@ -0,0 +1,221 @@ +#!/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()