Skip to content

Commit f21d9fd

Browse files
authored
Merge pull request #168 from minos-framework/issue-151-publish-api-specs
#151 - OpenAPI and AsyncAPI specs
2 parents cae7f01 + 3c741be commit f21d9fd

File tree

9 files changed

+247
-2
lines changed

9 files changed

+247
-2
lines changed

packages/core/minos-microservice-networks/minos/networks/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,10 @@
126126
ScheduledRequestContent,
127127
ScheduledResponseException,
128128
)
129+
from .specs import (
130+
AsyncAPIService,
131+
OpenAPIService,
132+
)
129133
from .system import (
130134
SystemService,
131135
)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from .asyncapi import (
2+
AsyncAPIService,
3+
)
4+
from .openapi import (
5+
OpenAPIService,
6+
)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from itertools import (
2+
chain,
3+
)
4+
5+
from minos.common import (
6+
Config,
7+
)
8+
9+
from ..decorators import (
10+
EnrouteCollector,
11+
enroute,
12+
)
13+
from ..requests import (
14+
Request,
15+
Response,
16+
)
17+
18+
19+
class AsyncAPIService:
20+
def __init__(self, config: Config):
21+
self.config = config
22+
self.spec = SPECIFICATION_SCHEMA.copy()
23+
24+
@enroute.rest.command("/spec/asyncapi", "GET")
25+
def generate_specification(self, request: Request) -> Response:
26+
events = self.get_events()
27+
28+
for event in events:
29+
topic: str = event["topic"]
30+
event_spec = {}
31+
32+
self.spec["channels"][topic] = event_spec
33+
34+
return Response(self.spec)
35+
36+
def get_events(self) -> list[dict]:
37+
events = list()
38+
for name in self.config.get_services():
39+
decorators = EnrouteCollector(name, self.config).get_broker_event()
40+
events += [{"topic": decorator.topic} for decorator in set(chain(*decorators.values()))]
41+
42+
return events
43+
44+
45+
SPECIFICATION_SCHEMA = {
46+
"asyncapi": "2.3.0",
47+
"info": {"title": "", "version": ""},
48+
"channels": {},
49+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
from itertools import (
2+
chain,
3+
)
4+
from operator import (
5+
itemgetter,
6+
)
7+
8+
from minos.common import (
9+
Config,
10+
)
11+
12+
from ..decorators import (
13+
EnrouteCollector,
14+
enroute,
15+
)
16+
from ..requests import (
17+
Request,
18+
Response,
19+
)
20+
21+
22+
class OpenAPIService:
23+
def __init__(self, config: Config):
24+
self.config = config
25+
self.spec = SPECIFICATION_SCHEMA.copy()
26+
27+
# noinspection PyUnusedLocal
28+
@enroute.rest.command("/spec/openapi", "GET")
29+
def generate_specification(self, request: Request) -> Response:
30+
for endpoint in self.endpoints:
31+
url = endpoint["url"]
32+
method = endpoint["method"].lower()
33+
34+
if url in self.spec["paths"]:
35+
self.spec["paths"][url][method] = PATH_SCHEMA
36+
else:
37+
self.spec["paths"][url] = {method: PATH_SCHEMA}
38+
39+
return Response(self.spec)
40+
41+
@property
42+
def endpoints(self) -> list[dict]:
43+
endpoints = list()
44+
for name in self.config.get_services():
45+
decorators = EnrouteCollector(name, self.config).get_rest_command_query()
46+
endpoints += [
47+
{"url": decorator.url, "method": decorator.method} for decorator in set(chain(*decorators.values()))
48+
]
49+
50+
endpoints.sort(key=itemgetter("url", "method"))
51+
52+
return endpoints
53+
54+
55+
SPECIFICATION_SCHEMA = {
56+
"openapi": "3.0.0",
57+
"info": {
58+
"version": "1.0.0",
59+
"title": "Minos OpenAPI Spec",
60+
"description": "Minos OpenAPI Spec",
61+
},
62+
"paths": {},
63+
}
64+
65+
PATH_SCHEMA = {
66+
"responses": {"200": {"description": ""}},
67+
}

packages/core/minos-microservice-networks/tests/services/commands.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ class CommandService:
1111
def get_order_rest(self, request: Request) -> Response:
1212
return Response("get_order")
1313

14+
@enroute.rest.command(path="/order", method="DELETE")
15+
def delete_order_rest(self, request: Request) -> Response:
16+
return Response("delete_order")
17+
1418
@enroute.broker.command("GetOrder")
1519
def get_order_command(self, request: Request) -> Response:
1620
return BrokerResponse("get_order")
@@ -28,7 +32,7 @@ def update_order(self, request: Request) -> Response:
2832
return BrokerResponse("update_order")
2933

3034
@enroute.broker.event("TicketAdded")
31-
def ticket_added(self, request: Request) -> None:
35+
def ticket_added(self, request: Request) -> str:
3236
return "command_service_ticket_added"
3337

3438
@enroute.periodic.event("@daily")

packages/core/minos-microservice-networks/tests/test_networks/test_discovery/test_connectors.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,14 @@ async def test_subscription(self):
5555
await self.discovery.subscribe()
5656
self.assertEqual(1, mock.call_count)
5757
expected = call(
58-
self.ip, 8080, "Order", [{"url": "/order", "method": "GET"}, {"url": "/ticket", "method": "POST"}]
58+
self.ip,
59+
8080,
60+
"Order",
61+
[
62+
{"url": "/order", "method": "DELETE"},
63+
{"url": "/order", "method": "GET"},
64+
{"url": "/ticket", "method": "POST"},
65+
],
5966
)
6067
self.assertEqual(expected, mock.call_args)
6168

packages/core/minos-microservice-networks/tests/test_networks/test_specs/__init__.py

Whitespace-only changes.
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import unittest
2+
3+
from minos.common import (
4+
Config,
5+
)
6+
from minos.networks import (
7+
AsyncAPIService,
8+
EnrouteCollector,
9+
InMemoryRequest,
10+
RestCommandEnrouteDecorator,
11+
)
12+
from tests.utils import (
13+
CONFIG_FILE_PATH,
14+
)
15+
16+
17+
class TestAsyncAPIService(unittest.IsolatedAsyncioTestCase):
18+
def setUp(self) -> None:
19+
super().setUp()
20+
self.config = Config(CONFIG_FILE_PATH)
21+
22+
def test_constructor(self):
23+
service = AsyncAPIService(self.config)
24+
self.assertIsInstance(service, AsyncAPIService)
25+
self.assertEqual(self.config, service.config)
26+
27+
def test_get_enroute(self):
28+
service = AsyncAPIService(self.config)
29+
expected = {
30+
service.generate_specification.__name__: {RestCommandEnrouteDecorator("/spec/asyncapi", "GET")},
31+
}
32+
observed = EnrouteCollector(service, self.config).get_all()
33+
self.assertEqual(expected, observed)
34+
35+
async def test_generate_spec(self):
36+
service = AsyncAPIService(self.config)
37+
38+
request = InMemoryRequest()
39+
response = service.generate_specification(request)
40+
41+
expected = {
42+
"asyncapi": "2.3.0",
43+
"info": {"title": "", "version": ""},
44+
"channels": {"TicketAdded": {}, "TicketDeleted": {}},
45+
}
46+
47+
self.assertEqual(expected, await response.content())
48+
49+
50+
if __name__ == "__main__":
51+
unittest.main()
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import unittest
2+
3+
from minos.common import (
4+
Config,
5+
)
6+
from minos.networks import (
7+
EnrouteCollector,
8+
InMemoryRequest,
9+
OpenAPIService,
10+
RestCommandEnrouteDecorator,
11+
)
12+
from tests.utils import (
13+
CONFIG_FILE_PATH,
14+
)
15+
16+
17+
class TestOpenAPIService(unittest.IsolatedAsyncioTestCase):
18+
def setUp(self) -> None:
19+
super().setUp()
20+
self.config = Config(CONFIG_FILE_PATH)
21+
22+
def test_constructor(self):
23+
service = OpenAPIService(self.config)
24+
self.assertIsInstance(service, OpenAPIService)
25+
self.assertEqual(self.config, service.config)
26+
27+
def test_get_enroute(self):
28+
service = OpenAPIService(self.config)
29+
expected = {
30+
service.generate_specification.__name__: {RestCommandEnrouteDecorator("/spec/openapi", "GET")},
31+
}
32+
observed = EnrouteCollector(service, self.config).get_all()
33+
self.assertEqual(expected, observed)
34+
35+
async def test_generate_spec(self):
36+
service = OpenAPIService(self.config)
37+
38+
request = InMemoryRequest()
39+
response = service.generate_specification(request)
40+
41+
expected = {
42+
"openapi": "3.0.0",
43+
"info": {"version": "1.0.0", "title": "Minos OpenAPI Spec", "description": "Minos OpenAPI Spec"},
44+
"paths": {
45+
"/order": {
46+
"delete": {"responses": {"200": {"description": ""}}},
47+
"get": {"responses": {"200": {"description": ""}}},
48+
},
49+
"/ticket": {"post": {"responses": {"200": {"description": ""}}}},
50+
},
51+
}
52+
53+
self.assertEqual(expected, await response.content())
54+
55+
56+
if __name__ == "__main__":
57+
unittest.main()

0 commit comments

Comments
 (0)