Repository: sparckles/Robyn (python)

Issue:
feat: override openapi json
let the user override the openapi json file. refrain from generating, if:

a) the file is present somewhere, OR
b) the user passes it as a param to Robyn()

Pull request:
feat: openapi config override
## Description

This PR fixes #961

## Summary

This PR does add openapi config override feat

## PR Checklist

Please ensure that:

- [x] The PR contains a descriptive title
- [x] The PR contains a descriptive summary of the changes
- [x] You build and test your changes before submitting a PR.
- [x] You have added relevant documentation
- [ ] You have added relevant tests. We prefer integration tests wherever possible

## Pre-Commit Instructions:

- [x] Ensure that you have run the [pre-commit hooks](https://github.com/sparckles/robyn#%EF%B8%8F-to-develop-locally) on your PR.



Patch:
diff --git a/docs_src/src/pages/documentation/api_reference/openapi.mdx b/docs_src/src/pages/documentation/api_reference/openapi.mdx
index b732fc1..6159f87 100644
--- a/docs_src/src/pages/documentation/api_reference/openapi.mdx
+++ b/docs_src/src/pages/documentation/api_reference/openapi.mdx
@@ -11,6 +11,10 @@ Out of the box, the following endpoints are setup for you:
 - `/docs` The Swagger UI
 - `/openapi.json` The JSON Specification
 
+To use a custom openapi configuration, you can:
+
+    - Place the `openapi.json` config file in the root directory.
+    - Or, pass the file path to the `openapi_file_path` parameter in the `Robyn()` constructor. (the parameter gets priority over the file).
 
 However, if you don't want to generate the OpenAPI docs, you can disable it by passing `--disable-openapi` flag while starting the application.
 
diff --git a/docs_src/src/pages/documentation/example_app/openapi.mdx b/docs_src/src/pages/documentation/example_app/openapi.mdx
index 9a0a693..8a3bbe7 100644
--- a/docs_src/src/pages/documentation/example_app/openapi.mdx
+++ b/docs_src/src/pages/documentation/example_app/openapi.mdx
@@ -13,6 +13,11 @@ Out of the box, the following endpoints are setup for you:
 
 However, if you don't want to generate the OpenAPI docs, you can disable it by passing `--disable-openapi` flag while starting the application.
 
+To use a custom openapi configuration, you can:
+
+    - Place the `openapi.json` config file in the root directory.
+    - Or, pass the file path to the `openapi_file_path` parameter in the `Robyn()` constructor. (the parameter gets priority over the file).
+
 ```bash
 python app.py --disable-openapi
 ```
diff --git a/robyn/__init__.py b/robyn/__init__.py
index 09e9a20..f5222ac 100644
--- a/robyn/__init__.py
+++ b/robyn/__init__.py
@@ -2,6 +2,7 @@ import asyncio
 import logging
 import os
 import socket
+from pathlib import Path
 from typing import Callable, List, Optional, Tuple, Union
 
 import multiprocess as mp
@@ -41,6 +42,7 @@ class Robyn:
         self,
         file_object: str,
         config: Config = Config(),
+        openapi_file_path: str = None,
         openapi: OpenAPI = OpenAPI(),
         dependencies: DependencyMap = DependencyMap(),
     ) -> None:
@@ -51,6 +53,11 @@ class Robyn:
         self.dependencies = dependencies
         self.openapi = openapi
 
+        if openapi_file_path:
+            openapi.override_openapi(Path(self.directory_path).joinpath(openapi_file_path))
+        elif Path(self.directory_path).joinpath("openapi.json").exists():
+            openapi.override_openapi(Path(self.directory_path).joinpath("openapi.json"))
+
         if not bool(os.environ.get("ROBYN_CLI", False)):
             # the env variables are already set when are running through the cli
             load_vars(project_root=directory_path)
@@ -583,7 +590,7 @@ class Robyn:
 
 class SubRouter(Robyn):
     def __init__(self, file_object: str, prefix: str = "", config: Config = Config(), openapi: OpenAPI = OpenAPI()) -> None:
-        super().__init__(file_object, config, openapi)
+        super().__init__(file_object=file_object, config=config, openapi=openapi)
         self.prefix = prefix
 
     def __add_prefix(self, endpoint: str):
diff --git a/robyn/openapi.py b/robyn/openapi.py
index e04d2fa..5d4868c 100644
--- a/robyn/openapi.py
+++ b/robyn/openapi.py
@@ -1,8 +1,10 @@
 import inspect
+import json
 import typing
 from dataclasses import asdict, dataclass, field
 from importlib import resources
 from inspect import Signature
+from pathlib import Path
 from typing import Any, Callable, Dict, List, Optional, TypedDict
 
 from robyn.responses import FileResponse, html
@@ -138,11 +140,15 @@ class OpenAPI:
 
     info: OpenAPIInfo = field(default_factory=OpenAPIInfo)
     openapi_spec: dict = field(init=False)
+    openapi_file_override: bool = False  # denotes whether there is an override or not.
 
     def __post_init__(self):
         """
         Initializes the openapi_spec dict
         """
+        if self.openapi_file_override:
+            return
+
         self.openapi_spec = {
             "openapi": "3.1.0",
             "info": asdict(self.info),
@@ -163,6 +169,9 @@ class OpenAPI:
         @param handler: Callable the handler function for the endpoint
         """
 
+        if self.openapi_file_override:
+            return
+
         query_params = None
         request_body = None
         return_annotation = None
@@ -212,6 +221,10 @@ class OpenAPI:
 
         @param subrouter_openapi: OpenAPI the OpenAPI object of the current subrouter
         """
+
+        if self.openapi_file_override:
+            return
+
         paths = subrouter_openapi.openapi_spec["paths"]
 
         for path in paths:
@@ -393,6 +406,16 @@ class OpenAPI:
 
         return properties
 
+    def override_openapi(self, openapi_json_spec_path: Path):
+        """
+        Set a pre-configured OpenAPI spec
+        @param openapi_json_spec_path: str the path to the json file
+        """
+        with open(openapi_json_spec_path) as json_file:
+            json_file_content = json.load(json_file)
+            self.openapi_spec = dict(json_file_content)
+            self.openapi_file_override = True
+
     def get_openapi_docs_page(self) -> FileResponse:
         """
         Handler to the swagger html page to be deployed to the endpoint `/docs`


Test patch:
diff --git a/integration_tests/openapi_config.json b/integration_tests/openapi_config.json
new file mode 100644
index 0000000..e21e022
--- /dev/null
+++ b/integration_tests/openapi_config.json
@@ -0,0 +1,69 @@
+{
+  "openapi": "3.1.0",
+  "info": {
+    "title": "Robyn Test API",
+    "version": "1.0.0",
+    "description": null,
+    "termsOfService": null,
+    "contact": {
+      "name": null,
+      "url": null,
+      "email": null
+    },
+    "license": {
+      "name": null,
+      "url": null
+    },
+    "servers": [],
+    "externalDocs": {
+      "description": null,
+      "url": null
+    },
+    "components": {
+      "schemas": {},
+      "responses": {},
+      "parameters": {},
+      "examples": {},
+      "requestBodies": {},
+      "securitySchemes": {},
+      "links": {},
+      "callbacks": {},
+      "pathItems": {}
+    }
+  },
+  "paths": {
+    "/": {
+      "get": {
+        "summary": "",
+        "description": "No description provided",
+        "parameters": [],
+        "tags": [
+          "get"
+        ],
+        "responses": {
+          "200": {
+            "description": "Successful Response",
+            "content": {
+              "text/plain": {
+                "schema": {}
+              }
+            }
+          }
+        }
+      }
+    }
+  },
+  "components": {
+    "schemas": {},
+    "responses": {},
+    "parameters": {},
+    "examples": {},
+    "requestBodies": {},
+    "securitySchemes": {},
+    "links": {},
+    "callbacks": {},
+    "pathItems": {}
+  },
+  "servers": [],
+  "externalDocs": null
+}
\ No newline at end of file
diff --git a/integration_tests/test_openapi.py b/integration_tests/test_openapi.py
index bf163b9..18de22a 100644
--- a/integration_tests/test_openapi.py
+++ b/integration_tests/test_openapi.py
@@ -1,6 +1,26 @@
 import pytest
 
 from integration_tests.helpers.http_methods_helpers import get
+from robyn import Robyn
+
+
+@pytest.mark.benchmark
+def test_custom_openapi_spec():
+    app = Robyn(__file__, openapi_file_path="openapi_config.json")
+
+    openapi_spec = app.openapi.openapi_spec
+
+    assert isinstance(openapi_spec, dict)
+
+    assert "openapi" in openapi_spec
+    assert "info" in openapi_spec
+    assert "paths" in openapi_spec
+    assert "components" in openapi_spec
+    assert "servers" in openapi_spec
+    assert "externalDocs" in openapi_spec
+
+    assert openapi_spec["info"]["title"] == "Robyn Test API"
+    assert openapi_spec["info"]["version"] == "1.0.0"
 
 
 @pytest.mark.benchmark