diff --git a/packages/core/minos-microservice-networks/minos/networks/__init__.py b/packages/core/minos-microservice-networks/minos/networks/__init__.py index a1645bae9..bd31d2eee 100644 --- a/packages/core/minos-microservice-networks/minos/networks/__init__.py +++ b/packages/core/minos-microservice-networks/minos/networks/__init__.py @@ -126,6 +126,10 @@ ScheduledRequestContent, ScheduledResponseException, ) +from .specs import ( + AsyncAPIService, + OpenAPIService, +) from .system import ( SystemService, ) diff --git a/packages/core/minos-microservice-networks/minos/networks/specs/__init__.py b/packages/core/minos-microservice-networks/minos/networks/specs/__init__.py new file mode 100644 index 000000000..8e603f2b8 --- /dev/null +++ b/packages/core/minos-microservice-networks/minos/networks/specs/__init__.py @@ -0,0 +1,6 @@ +from .asyncapi import ( + AsyncAPIService, +) +from .openapi import ( + OpenAPIService, +) diff --git a/packages/core/minos-microservice-networks/minos/networks/specs/asyncapi.py b/packages/core/minos-microservice-networks/minos/networks/specs/asyncapi.py new file mode 100644 index 000000000..3fd5e2116 --- /dev/null +++ b/packages/core/minos-microservice-networks/minos/networks/specs/asyncapi.py @@ -0,0 +1,49 @@ +from itertools import ( + chain, +) + +from minos.common import ( + Config, +) + +from ..decorators import ( + EnrouteCollector, + enroute, +) +from ..requests import ( + Request, + Response, +) + + +class AsyncAPIService: + def __init__(self, config: Config): + self.config = config + self.spec = SPECIFICATION_SCHEMA.copy() + + @enroute.rest.command("/spec/asyncapi", "GET") + def generate_specification(self, request: Request) -> Response: + events = self.get_events() + + for event in events: + topic: str = event["topic"] + event_spec = {} + + self.spec["channels"][topic] = event_spec + + return Response(self.spec) + + def get_events(self) -> list[dict]: + events = list() + for name in self.config.get_services(): + decorators = EnrouteCollector(name, self.config).get_broker_event() + events += [{"topic": decorator.topic} for decorator in set(chain(*decorators.values()))] + + return events + + +SPECIFICATION_SCHEMA = { + "asyncapi": "2.3.0", + "info": {"title": "", "version": ""}, + "channels": {}, +} diff --git a/packages/core/minos-microservice-networks/minos/networks/specs/openapi.py b/packages/core/minos-microservice-networks/minos/networks/specs/openapi.py new file mode 100644 index 000000000..96ec32a3e --- /dev/null +++ b/packages/core/minos-microservice-networks/minos/networks/specs/openapi.py @@ -0,0 +1,67 @@ +from itertools import ( + chain, +) +from operator import ( + itemgetter, +) + +from minos.common import ( + Config, +) + +from ..decorators import ( + EnrouteCollector, + enroute, +) +from ..requests import ( + Request, + Response, +) + + +class OpenAPIService: + def __init__(self, config: Config): + self.config = config + self.spec = SPECIFICATION_SCHEMA.copy() + + # noinspection PyUnusedLocal + @enroute.rest.command("/spec/openapi", "GET") + def generate_specification(self, request: Request) -> Response: + for endpoint in self.endpoints: + url = endpoint["url"] + method = endpoint["method"].lower() + + if url in self.spec["paths"]: + self.spec["paths"][url][method] = PATH_SCHEMA + else: + self.spec["paths"][url] = {method: PATH_SCHEMA} + + return Response(self.spec) + + @property + def endpoints(self) -> list[dict]: + endpoints = list() + for name in self.config.get_services(): + decorators = EnrouteCollector(name, self.config).get_rest_command_query() + endpoints += [ + {"url": decorator.url, "method": decorator.method} for decorator in set(chain(*decorators.values())) + ] + + endpoints.sort(key=itemgetter("url", "method")) + + return endpoints + + +SPECIFICATION_SCHEMA = { + "openapi": "3.0.0", + "info": { + "version": "1.0.0", + "title": "Minos OpenAPI Spec", + "description": "Minos OpenAPI Spec", + }, + "paths": {}, +} + +PATH_SCHEMA = { + "responses": {"200": {"description": ""}}, +} diff --git a/packages/core/minos-microservice-networks/tests/services/commands.py b/packages/core/minos-microservice-networks/tests/services/commands.py index 23f3c708c..fbbc75cd9 100644 --- a/packages/core/minos-microservice-networks/tests/services/commands.py +++ b/packages/core/minos-microservice-networks/tests/services/commands.py @@ -11,6 +11,10 @@ class CommandService: def get_order_rest(self, request: Request) -> Response: return Response("get_order") + @enroute.rest.command(path="/order", method="DELETE") + def delete_order_rest(self, request: Request) -> Response: + return Response("delete_order") + @enroute.broker.command("GetOrder") def get_order_command(self, request: Request) -> Response: return BrokerResponse("get_order") @@ -28,7 +32,7 @@ def update_order(self, request: Request) -> Response: return BrokerResponse("update_order") @enroute.broker.event("TicketAdded") - def ticket_added(self, request: Request) -> None: + def ticket_added(self, request: Request) -> str: return "command_service_ticket_added" @enroute.periodic.event("@daily") diff --git a/packages/core/minos-microservice-networks/tests/test_networks/test_discovery/test_connectors.py b/packages/core/minos-microservice-networks/tests/test_networks/test_discovery/test_connectors.py index 304955766..a709f25f5 100644 --- a/packages/core/minos-microservice-networks/tests/test_networks/test_discovery/test_connectors.py +++ b/packages/core/minos-microservice-networks/tests/test_networks/test_discovery/test_connectors.py @@ -55,7 +55,14 @@ async def test_subscription(self): await self.discovery.subscribe() self.assertEqual(1, mock.call_count) expected = call( - self.ip, 8080, "Order", [{"url": "/order", "method": "GET"}, {"url": "/ticket", "method": "POST"}] + self.ip, + 8080, + "Order", + [ + {"url": "/order", "method": "DELETE"}, + {"url": "/order", "method": "GET"}, + {"url": "/ticket", "method": "POST"}, + ], ) self.assertEqual(expected, mock.call_args) diff --git a/packages/core/minos-microservice-networks/tests/test_networks/test_specs/__init__.py b/packages/core/minos-microservice-networks/tests/test_networks/test_specs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/packages/core/minos-microservice-networks/tests/test_networks/test_specs/test_asyncapi.py b/packages/core/minos-microservice-networks/tests/test_networks/test_specs/test_asyncapi.py new file mode 100644 index 000000000..d0cda344d --- /dev/null +++ b/packages/core/minos-microservice-networks/tests/test_networks/test_specs/test_asyncapi.py @@ -0,0 +1,51 @@ +import unittest + +from minos.common import ( + Config, +) +from minos.networks import ( + AsyncAPIService, + EnrouteCollector, + InMemoryRequest, + RestCommandEnrouteDecorator, +) +from tests.utils import ( + CONFIG_FILE_PATH, +) + + +class TestAsyncAPIService(unittest.IsolatedAsyncioTestCase): + def setUp(self) -> None: + super().setUp() + self.config = Config(CONFIG_FILE_PATH) + + def test_constructor(self): + service = AsyncAPIService(self.config) + self.assertIsInstance(service, AsyncAPIService) + self.assertEqual(self.config, service.config) + + def test_get_enroute(self): + service = AsyncAPIService(self.config) + expected = { + service.generate_specification.__name__: {RestCommandEnrouteDecorator("/spec/asyncapi", "GET")}, + } + observed = EnrouteCollector(service, self.config).get_all() + self.assertEqual(expected, observed) + + async def test_generate_spec(self): + service = AsyncAPIService(self.config) + + request = InMemoryRequest() + response = service.generate_specification(request) + + expected = { + "asyncapi": "2.3.0", + "info": {"title": "", "version": ""}, + "channels": {"TicketAdded": {}, "TicketDeleted": {}}, + } + + self.assertEqual(expected, await response.content()) + + +if __name__ == "__main__": + unittest.main() diff --git a/packages/core/minos-microservice-networks/tests/test_networks/test_specs/test_openapi.py b/packages/core/minos-microservice-networks/tests/test_networks/test_specs/test_openapi.py new file mode 100644 index 000000000..7e2374f23 --- /dev/null +++ b/packages/core/minos-microservice-networks/tests/test_networks/test_specs/test_openapi.py @@ -0,0 +1,57 @@ +import unittest + +from minos.common import ( + Config, +) +from minos.networks import ( + EnrouteCollector, + InMemoryRequest, + OpenAPIService, + RestCommandEnrouteDecorator, +) +from tests.utils import ( + CONFIG_FILE_PATH, +) + + +class TestOpenAPIService(unittest.IsolatedAsyncioTestCase): + def setUp(self) -> None: + super().setUp() + self.config = Config(CONFIG_FILE_PATH) + + def test_constructor(self): + service = OpenAPIService(self.config) + self.assertIsInstance(service, OpenAPIService) + self.assertEqual(self.config, service.config) + + def test_get_enroute(self): + service = OpenAPIService(self.config) + expected = { + service.generate_specification.__name__: {RestCommandEnrouteDecorator("/spec/openapi", "GET")}, + } + observed = EnrouteCollector(service, self.config).get_all() + self.assertEqual(expected, observed) + + async def test_generate_spec(self): + service = OpenAPIService(self.config) + + request = InMemoryRequest() + response = service.generate_specification(request) + + expected = { + "openapi": "3.0.0", + "info": {"version": "1.0.0", "title": "Minos OpenAPI Spec", "description": "Minos OpenAPI Spec"}, + "paths": { + "/order": { + "delete": {"responses": {"200": {"description": ""}}}, + "get": {"responses": {"200": {"description": ""}}}, + }, + "/ticket": {"post": {"responses": {"200": {"description": ""}}}}, + }, + } + + self.assertEqual(expected, await response.content()) + + +if __name__ == "__main__": + unittest.main()