#!/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) -> 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", "var base_url: String", "", "func _init(node: Node, base_url_param: String):", " http_request = HTTPRequest.new()", " node.add_child(http_request)", ' http_request.connect("request_completed", self, "_on_request_completed")', " base_url = base_url_param", "", ] # 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") lines.extend([ f"func {func_name}(params: Dictionary = {{}}, callback: Callable):", f' var url := base_url + "{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): """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) 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") args = parser.parse_args() spec = load_openapi_spec(args.input) generate_code(spec, args.output) print("Done!") print("Note: When initializing the API classes, pass the base URL as a parameter:") print(" var music_api = MusicAPI.new()") print(" music_api._init(get_node('/root'), 'http://localhost:8080')") if __name__ == "__main__": main()