From 144386d522c8693dc4a02424ef7786ed0bd133ff Mon Sep 17 00:00:00 2001 From: rayangler <27821750+rayangler@users.noreply.github.com> Date: Mon, 31 Oct 2022 10:56:25 -0400 Subject: [PATCH] DOP-3307: Generate metadata for OpenAPI content pages (#426) --- snooty/parser.py | 3 ++ snooty/postprocess.py | 62 +++++++++++++++++++++++++ snooty/rstspec.toml | 1 + snooty/test_openapi.py | 6 +-- snooty/test_postprocess.py | 92 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 161 insertions(+), 3 deletions(-) diff --git a/snooty/parser.py b/snooty/parser.py index 1907a9d9..26b37420 100644 --- a/snooty/parser.py +++ b/snooty/parser.py @@ -662,6 +662,7 @@ def handle_directive( spec = json.dumps(safe_load(file_content)) spec_node = n.Text((line,), spec) doc.children.append(spec_node) + doc.options["source_type"] = "url" return doc except: pass @@ -672,6 +673,7 @@ def handle_directive( return doc if uses_realm: + doc.options["source_type"] = "atlas" return doc openapi_fileid, filepath = util.reroot_path( @@ -710,6 +712,7 @@ def create_page() -> Tuple[Page, EmbeddedRstParser]: spec = json.dumps(safe_load(f)) spec_node = n.Text((line,), spec) doc.children.append(spec_node) + doc.options["source_type"] = "local" except OSError as err: self.diagnostics.append( diff --git a/snooty/postprocess.py b/snooty/postprocess.py index f2b295c3..3ce315a0 100644 --- a/snooty/postprocess.py +++ b/snooty/postprocess.py @@ -919,6 +919,62 @@ def enter_node(self, fileid_stack: FileIdStack, node: n.Node) -> None: self.guides[current_slug].description = node.children +class OpenAPIHandler(Handler): + """Constructs metadata for OpenAPI content pages.""" + + @dataclass + class SourceData: + source_type: str + source: str + + def __init__(self, context: Context) -> None: + super().__init__(context) + self.openapi_pages: Dict[str, OpenAPIHandler.SourceData] = {} + + def get_metadata(self) -> Dict[str, SerializableType]: + """Returns serialized object to be used as part of the build's metadata.""" + + return {k: asdict(v) for k, v in self.openapi_pages.items()} + + def enter_node(self, fileid_stack: FileIdStack, node: n.Node) -> None: + if ( + not isinstance(node, n.Directive) + or node.name != "openapi" + or node.options.get("preview") + ): + return + + current_file = fileid_stack.current + current_slug = clean_slug(current_file.without_known_suffix) + + if current_slug in self.openapi_pages: + self.context.diagnostics[current_file].append( + DuplicateDirective(node.name, node.start[0]) + ) + return + + # source_type should be assigned in the parsing layer + source_type = node.options.get("source_type") + if not source_type: + return + + source = "" + argument = node.argument[0] + # The parser determines the source_type based on the given argument and its + # node structure. We echo that logic here to grab the source without needing + # to worry about the argument's node structure. + # The source_type cannot be manually set in rST as long as the option is not exposed + # in the rstspec. + if source_type == "local" or source_type == "atlas": + assert isinstance(argument, n.Text) + source = argument.get_text() + else: + assert isinstance(argument, n.Reference) + source = argument.refuri + + self.openapi_pages[current_slug] = self.SourceData(source_type, source) + + class IAHandler(Handler): """Identify IA directive on a page and save a list of its entries as a page-level option.""" @@ -1443,6 +1499,7 @@ class Postprocessor: ContentsHandler, BannerHandler, GuidesHandler, + OpenAPIHandler, ], [TargetHandler, IAHandler, NamedReferenceHandlerPass1], [RefsHandler, NamedReferenceHandlerPass2], @@ -1541,6 +1598,10 @@ def generate_metadata(cls, context: Context) -> n.SerializedNode: context[GuidesHandler].add_guides_metadata(document) + openapi_pages_metadata = context[OpenAPIHandler].get_metadata() + if len(openapi_pages_metadata) > 0: + document["openapi_pages"] = openapi_pages_metadata + manpages = build_manpages(context) document["static_files"] = manpages @@ -1919,6 +1980,7 @@ class DevhubPostprocessor(Postprocessor): ContentsHandler, BannerHandler, GuidesHandler, + OpenAPIHandler, ], [TargetHandler, IAHandler, NamedReferenceHandlerPass1], [RefsHandler, NamedReferenceHandlerPass2, DevhubHandler], diff --git a/snooty/rstspec.toml b/snooty/rstspec.toml index f4aeecff..896e1f97 100644 --- a/snooty/rstspec.toml +++ b/snooty/rstspec.toml @@ -836,6 +836,7 @@ help = """Include a reStructuredText file's contents.""" argument_type = ["path", "string", "uri"] options.uses-rst = "flag" options.uses-realm = "flag" +options.preview = "flag" [directive."mongodb:operation"] content_type = "block" diff --git a/snooty/test_openapi.py b/snooty/test_openapi.py index 03d3cf26..653ba004 100644 --- a/snooty/test_openapi.py +++ b/snooty/test_openapi.py @@ -138,7 +138,7 @@ def test_openapi_using_filepath() -> None: check_ast_testing_string( page.ast, """ -/test_parser/openapi-admin-v3.yaml +/test_parser/openapi-admin-v3.yaml {"openapi": "3.0.1", "info": {"description": "", "version": "3.0", "title": "MongoDB Realm API"}, "servers": [{"url": "https://realm.mongodb.com/api/admin/v3.0", "description": "The root API resource and starting point for the Realm API."}], "paths": {"/groups/{groupId}/apps/{appId}/services/{serviceId}": {"get": {"tags": ["services"], "operationId": "adminGetService", "summary": "Retrieve a :ref:`service <services>`.", "description": "Test description here.\\n", "responses": {"200": {"description": "The service was successfully deleted.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Service"}}}}}}, "delete": {"tags": ["services"], "operationId": "adminDeleteService", "summary": "Delete a :ref:`service <services>`.", "responses": {"204": {"description": "The service was successfully deleted."}}}, "patch": {"tags": ["services"], "operationId": "adminUpdateService", "summary": "Update a :ref:`service <services>`.", "responses": {"200": {"description": "Successfully updated."}}}, "parameters": [{"$ref": "#/components/parameters/GroupId"}, {"$ref": "#/components/parameters/AppId"}, {"$ref": "#/components/parameters/ServiceId"}]}, "/groups/{groupId}/apps/{appId}/logs": {"get": {"tags": ["logs"], "operationId": "adminGetLogs", "summary": "Retrieve MongoDB Realm logs.", "parameters": [{"name": "co_id", "in": "query", "description": "Return only log messages associated with the given request ID.", "schema": {"type": "string"}, "required": false}, {"name": "errors_only", "in": "query", "description": "Whether to only return errors.", "schema": {"type": "boolean"}, "required": false}, {"name": "user_id", "in": "query", "schema": {"type": "string"}, "description": "Return only log messages associated with the given ``user_id``.", "required": false}, {"name": "start_date", "in": "query", "schema": {"type": "string"}, "description": "The date and time in ISO 8601 at which to begin returning results, exclusive.", "required": false}, {"name": "end_date", "in": "query", "schema": {"type": "string"}, "description": "The date and time in ISO 8601 at which to cease returning results, inclusive.", "required": false}, {"name": "skip", "in": "query", "schema": {"type": "integer"}, "description": "The offset number of matching log entries to skip before including them in the response.", "required": false, "default": 0}, {"name": "limit", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 100}, "default": 100, "description": "The maximum number of log entries to include in the response. If the\\nquery matches more than this many logs, it returns documents in\\nascending order by date until the limit is reached.\\n", "required": false}], "responses": {"200": {"description": "Successfully retrieved.", "content": {"application/json": {"schema": {"type": "object", "properties": {"logs": {"type": "array", "items": {"type": "object", "properties": {"_id": {"type": "string"}, "co_id": {"type": "string"}, "domain_id": {"type": "string"}, "app_id": {"$ref": "#/components/parameters/AppId"}, "group_id": {"$ref": "#/components/parameters/GroupId"}, "request_url": {"type": "string"}, "request_method": {"type": "string"}, "started": {"type": "string"}, "completed": {"type": "string"}, "error": {"type": "string"}, "error_code": {"type": "string"}, "status": {"type": "integer"}}}}, "nextEndDate": {"type": "string", "required": false, "description": "The end date and time of the next page of log entries in ISO 8601 format. MongoDB Realm paginates the result sets of queries that match more than 100 log entries and includes this field in paginated responses. To get the next page of up to 100 entries, pass this value as the ``end_date`` parameter in a subsequent request."}, "nextSkip": {"type": "integer", "required": false, "description": "The offset into the next page of log entries in ISO 8601 format. MongoDB Realm paginates the result sets of queries that match more than 100 log entries and includes this field in paginated responses where the first entry on the next page has the same timestamp as the last entry on this page. To get the next page of up to 100 entries, pass this value, if it is present, as the ``skip`` parameter in a subsequent request."}}}}}}}}, "parameters": [{"$ref": "#/components/parameters/GroupId"}, {"$ref": "#/components/parameters/AppId"}]}, "/groups/{groupId}/apps/{appId}/api_keys": {"get": {"tags": ["apikeys"], "operationId": "adminListApiKeys", "summary": "List :doc:`API keys </authentication/api-key>` associated with a Realm app.", "responses": {"200": {"description": "The API keys were successfully listed.", "content": {"application/json": {"schema": {"items": {"properties": {"_id": {"type": "string"}, "name": {"type": "string"}, "disabled": {"type": "boolean"}}}}}}}}}, "post": {"tags": ["apikeys"], "operationId": "adminCreateApiKey", "summary": "Create a new :doc:`API key </authentication/api-key>`.", "requestBody": {"description": "The API key to create.", "required": true, "content": {"application/json": {"schema": {"properties": {"name": {"type": "string"}}, "required": ["name"]}}}}, "responses": {"201": {"description": "The API key was successfully created.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ApiKey"}}}}}}, "parameters": [{"$ref": "#/components/parameters/GroupId"}, {"$ref": "#/components/parameters/AppId"}]}, "/groups/{groupId}/apps/{appId}/measurements/": {"get": {"tags": ["billing"], "operationId": "adminAppMeasurements", "summary": "List the request, compute, sync, data transfer, and memory usage of a specific app in a given period for :doc:`billing </billing>` purposes.", "parameters": [{"$ref": "#/components/parameters/GroupId"}, {"$ref": "#/components/parameters/AppId"}, {"name": "start", "in": "query", "description": "The ISO 8601 date and time of the start of the query period. Default is 00:00:00 UTC on the first day of the current month.", "schema": {"type": "string"}, "required": false}, {"name": "end", "in": "query", "description": "The ISO 8601 date and time of the end of the query period. Default is 23:59:59 UTC on the the last day of the current month.", "schema": {"type": "string"}, "required": false}, {"name": "granularity", "in": "query", "description": "Specifies the granularity of the query period, either P31D (31 day) or PT1H (1 hour). Default is P31D.", "schema": {"type": "string", "enum": ["P31D", "PT1H"]}, "required": false}], "responses": {"200": {"description": "The measurements were successfully returned.", "content": {"application/json": {"schema": {"properties": {"start": {"type": "string", "description": "The RFC 3339 date and time of the start of the query period, which can be specified with the ``start`` query parameter."}, "end": {"type": "string", "description": "The RFC 3339 date and time of the end of the query period, which can be specified with the ``end`` query parameter."}, "granularity": {"type": "string", "description": "The granularity, which can be specified with the ``granularity`` query parameter."}, "group_id": {"type": "string", "description": "The |atlas| :atlas:`Group ID </tutorial/manage-projects/>`."}, "appId": {"type": "string", "description": "The Realm app ID specified by the ``appId`` path parameter."}, "appName": {"type": "string", "description": "The name of the Realm app specified by the ``appId`` path parameter."}, "measurements": {"type": "array", "description": "The array of measurements.\\n", "items": {"properties": {"name": {"type": "string", "enum": ["request_count", "compute_time", "data_out", "sync_time", "mem_usage"], "description": "The usage metric represented by each data point. See :doc:`billing </billing>`. \\n"}, "units": {"type": "string", "enum": ["<empty string>", "HOURS", "GIGABYTES", "GIGABYTE_SECONDS"], "description": "The unit of the ``value`` of each data point.\\n"}, "data_points": {"type": "array", "description": "The array of data points for this measurement. A finer ``granularity`` results in more data points.\\n", "items": {"properties": {"timestamp": {"type": "string", "description": "The ISO 8601 date and time of the data point.\\n"}, "value": {"type": "number", "description": "The value at the time in the ``unit`` of the measurement.\\n"}}}}}}}}}}}}, "400": {"$ref": "#/components/responses/ClientErrorResponse"}}}}}, "components": {"parameters": {"GroupId": {"name": "groupId", "description": "An |atlas| :atlas:`Project/Group ID </tutorial/manage-projects/>`.", "in": "path", "required": true, "schema": {"type": "string"}}, "AppId": {"name": "appId", "description": "The ObjectID of your application.\\n:ref:`realm-api-project-and-application-ids` demonstrates how\\nto find this value.\\n", "in": "path", "required": true, "schema": {"type": "string"}}, "ServiceId": {"name": "serviceId", "description": "Service ID", "in": "path", "required": true, "schema": {"type": "string"}}}, "schemas": {"ApiKey": {"properties": {"_id": {"type": "string"}, "key": {"type": "string"}, "name": {"type": "string"}, "disabled": {"type": "string"}}}, "Application": {"type": "object", "properties": {"_id": {"type": "string", "description": "The application's unique internal ID."}, "client_app_id": {"type": "string", "description": "The application's public App ID."}, "name": {"type": "string", "description": "The name of the application."}, "location": {"type": "string", "description": "The application's deployment region."}, "deployment_model": {"type": "string", "description": "The application's deployment model."}, "domain_id": {"type": "string"}, "group_id": {"$ref": "#/components/parameters/GroupId"}}}, "Service": {"properties": {"_id": {"type": "string"}, "name": {"type": "string"}, "type": {"type": "string"}, "version": {"type": "integer"}}}, "MetadataAttribute": {"type": "object", "properties": {"name": {"type": "string", "description": "The :doc:`metadata attribute </hosting/file-metadata-attributes>` name."}, "value": {"type": "string", "description": "The :doc:`metadata attribute </hosting/file-metadata-attributes>` value."}}}}, "securitySchemes": {"tokenAuth": {"type": "http", "scheme": "bearer", "description": "The authorization token provided in the ``access_token`` field of\\nthe :ref:`post-/auth/providers/{provider}/login` and\\n:ref:`post-/auth/session` API endpoints.\\n"}}, "responses": {"ClientErrorResponse": {"description": "There is an error in the request.", "content": {"application/json": {"schema": {"properties": {"error": {"type": "string", "description": "A message describing the error.\\n"}}}}}}}}, "tags": [{"name": "apikeys", "description": "API Key APIs"}, {"name": "billing", "description": "Billing APIs"}, {"name": "logs", "description": "Logging APIs"}, {"name": "services", "description": "Services APIs"}], "security": [{"tokenAuth": []}]} """, ) @@ -162,7 +162,7 @@ def test_openapi_using_url() -> None: check_ast_testing_string( page.ast, """ -https://raw.githubusercontent.com/mongodb/snooty-parser/master/test_data/test_parser/openapi-admin-v3.yaml +https://raw.githubusercontent.com/mongodb/snooty-parser/master/test_data/test_parser/openapi-admin-v3.yaml {"openapi": "3.0.1", "info": {"description": "", "version": "3.0", "title": "MongoDB Realm API"}, "servers": [{"url": "https://realm.mongodb.com/api/admin/v3.0", "description": "The root API resource and starting point for the Realm API."}], "paths": {"/groups/{groupId}/apps/{appId}/services/{serviceId}": {"get": {"tags": ["services"], "operationId": "adminGetService", "summary": "Retrieve a :ref:`service <services>`.", "description": "Test description here.\\n", "responses": {"200": {"description": "The service was successfully deleted.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Service"}}}}}}, "delete": {"tags": ["services"], "operationId": "adminDeleteService", "summary": "Delete a :ref:`service <services>`.", "responses": {"204": {"description": "The service was successfully deleted."}}}, "patch": {"tags": ["services"], "operationId": "adminUpdateService", "summary": "Update a :ref:`service <services>`.", "responses": {"200": {"description": "Successfully updated."}}}, "parameters": [{"$ref": "#/components/parameters/GroupId"}, {"$ref": "#/components/parameters/AppId"}, {"$ref": "#/components/parameters/ServiceId"}]}, "/groups/{groupId}/apps/{appId}/logs": {"get": {"tags": ["logs"], "operationId": "adminGetLogs", "summary": "Retrieve MongoDB Realm logs.", "parameters": [{"name": "co_id", "in": "query", "description": "Return only log messages associated with the given request ID.", "schema": {"type": "string"}, "required": false}, {"name": "errors_only", "in": "query", "description": "Whether to only return errors.", "schema": {"type": "boolean"}, "required": false}, {"name": "user_id", "in": "query", "schema": {"type": "string"}, "description": "Return only log messages associated with the given ``user_id``.", "required": false}, {"name": "start_date", "in": "query", "schema": {"type": "string"}, "description": "The date and time in ISO 8601 at which to begin returning results, exclusive.", "required": false}, {"name": "end_date", "in": "query", "schema": {"type": "string"}, "description": "The date and time in ISO 8601 at which to cease returning results, inclusive.", "required": false}, {"name": "skip", "in": "query", "schema": {"type": "integer"}, "description": "The offset number of matching log entries to skip before including them in the response.", "required": false, "default": 0}, {"name": "limit", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 100}, "default": 100, "description": "The maximum number of log entries to include in the response. If the\\nquery matches more than this many logs, it returns documents in\\nascending order by date until the limit is reached.\\n", "required": false}], "responses": {"200": {"description": "Successfully retrieved.", "content": {"application/json": {"schema": {"type": "object", "properties": {"logs": {"type": "array", "items": {"type": "object", "properties": {"_id": {"type": "string"}, "co_id": {"type": "string"}, "domain_id": {"type": "string"}, "app_id": {"$ref": "#/components/parameters/AppId"}, "group_id": {"$ref": "#/components/parameters/GroupId"}, "request_url": {"type": "string"}, "request_method": {"type": "string"}, "started": {"type": "string"}, "completed": {"type": "string"}, "error": {"type": "string"}, "error_code": {"type": "string"}, "status": {"type": "integer"}}}}, "nextEndDate": {"type": "string", "required": false, "description": "The end date and time of the next page of log entries in ISO 8601 format. MongoDB Realm paginates the result sets of queries that match more than 100 log entries and includes this field in paginated responses. To get the next page of up to 100 entries, pass this value as the ``end_date`` parameter in a subsequent request."}, "nextSkip": {"type": "integer", "required": false, "description": "The offset into the next page of log entries in ISO 8601 format. MongoDB Realm paginates the result sets of queries that match more than 100 log entries and includes this field in paginated responses where the first entry on the next page has the same timestamp as the last entry on this page. To get the next page of up to 100 entries, pass this value, if it is present, as the ``skip`` parameter in a subsequent request."}}}}}}}}, "parameters": [{"$ref": "#/components/parameters/GroupId"}, {"$ref": "#/components/parameters/AppId"}]}, "/groups/{groupId}/apps/{appId}/api_keys": {"get": {"tags": ["apikeys"], "operationId": "adminListApiKeys", "summary": "List :doc:`API keys </authentication/api-key>` associated with a Realm app.", "responses": {"200": {"description": "The API keys were successfully listed.", "content": {"application/json": {"schema": {"items": {"properties": {"_id": {"type": "string"}, "name": {"type": "string"}, "disabled": {"type": "boolean"}}}}}}}}}, "post": {"tags": ["apikeys"], "operationId": "adminCreateApiKey", "summary": "Create a new :doc:`API key </authentication/api-key>`.", "requestBody": {"description": "The API key to create.", "required": true, "content": {"application/json": {"schema": {"properties": {"name": {"type": "string"}}, "required": ["name"]}}}}, "responses": {"201": {"description": "The API key was successfully created.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ApiKey"}}}}}}, "parameters": [{"$ref": "#/components/parameters/GroupId"}, {"$ref": "#/components/parameters/AppId"}]}, "/groups/{groupId}/apps/{appId}/measurements/": {"get": {"tags": ["billing"], "operationId": "adminAppMeasurements", "summary": "List the request, compute, sync, data transfer, and memory usage of a specific app in a given period for :doc:`billing </billing>` purposes.", "parameters": [{"$ref": "#/components/parameters/GroupId"}, {"$ref": "#/components/parameters/AppId"}, {"name": "start", "in": "query", "description": "The ISO 8601 date and time of the start of the query period. Default is 00:00:00 UTC on the first day of the current month.", "schema": {"type": "string"}, "required": false}, {"name": "end", "in": "query", "description": "The ISO 8601 date and time of the end of the query period. Default is 23:59:59 UTC on the the last day of the current month.", "schema": {"type": "string"}, "required": false}, {"name": "granularity", "in": "query", "description": "Specifies the granularity of the query period, either P31D (31 day) or PT1H (1 hour). Default is P31D.", "schema": {"type": "string", "enum": ["P31D", "PT1H"]}, "required": false}], "responses": {"200": {"description": "The measurements were successfully returned.", "content": {"application/json": {"schema": {"properties": {"start": {"type": "string", "description": "The RFC 3339 date and time of the start of the query period, which can be specified with the ``start`` query parameter."}, "end": {"type": "string", "description": "The RFC 3339 date and time of the end of the query period, which can be specified with the ``end`` query parameter."}, "granularity": {"type": "string", "description": "The granularity, which can be specified with the ``granularity`` query parameter."}, "group_id": {"type": "string", "description": "The |atlas| :atlas:`Group ID </tutorial/manage-projects/>`."}, "appId": {"type": "string", "description": "The Realm app ID specified by the ``appId`` path parameter."}, "appName": {"type": "string", "description": "The name of the Realm app specified by the ``appId`` path parameter."}, "measurements": {"type": "array", "description": "The array of measurements.\\n", "items": {"properties": {"name": {"type": "string", "enum": ["request_count", "compute_time", "data_out", "sync_time", "mem_usage"], "description": "The usage metric represented by each data point. See :doc:`billing </billing>`. \\n"}, "units": {"type": "string", "enum": ["<empty string>", "HOURS", "GIGABYTES", "GIGABYTE_SECONDS"], "description": "The unit of the ``value`` of each data point.\\n"}, "data_points": {"type": "array", "description": "The array of data points for this measurement. A finer ``granularity`` results in more data points.\\n", "items": {"properties": {"timestamp": {"type": "string", "description": "The ISO 8601 date and time of the data point.\\n"}, "value": {"type": "number", "description": "The value at the time in the ``unit`` of the measurement.\\n"}}}}}}}}}}}}, "400": {"$ref": "#/components/responses/ClientErrorResponse"}}}}}, "components": {"parameters": {"GroupId": {"name": "groupId", "description": "An |atlas| :atlas:`Project/Group ID </tutorial/manage-projects/>`.", "in": "path", "required": true, "schema": {"type": "string"}}, "AppId": {"name": "appId", "description": "The ObjectID of your application.\\n:ref:`realm-api-project-and-application-ids` demonstrates how\\nto find this value.\\n", "in": "path", "required": true, "schema": {"type": "string"}}, "ServiceId": {"name": "serviceId", "description": "Service ID", "in": "path", "required": true, "schema": {"type": "string"}}}, "schemas": {"ApiKey": {"properties": {"_id": {"type": "string"}, "key": {"type": "string"}, "name": {"type": "string"}, "disabled": {"type": "string"}}}, "Application": {"type": "object", "properties": {"_id": {"type": "string", "description": "The application's unique internal ID."}, "client_app_id": {"type": "string", "description": "The application's public App ID."}, "name": {"type": "string", "description": "The name of the application."}, "location": {"type": "string", "description": "The application's deployment region."}, "deployment_model": {"type": "string", "description": "The application's deployment model."}, "domain_id": {"type": "string"}, "group_id": {"$ref": "#/components/parameters/GroupId"}}}, "Service": {"properties": {"_id": {"type": "string"}, "name": {"type": "string"}, "type": {"type": "string"}, "version": {"type": "integer"}}}, "MetadataAttribute": {"type": "object", "properties": {"name": {"type": "string", "description": "The :doc:`metadata attribute </hosting/file-metadata-attributes>` name."}, "value": {"type": "string", "description": "The :doc:`metadata attribute </hosting/file-metadata-attributes>` value."}}}}, "securitySchemes": {"tokenAuth": {"type": "http", "scheme": "bearer", "description": "The authorization token provided in the ``access_token`` field of\\nthe :ref:`post-/auth/providers/{provider}/login` and\\n:ref:`post-/auth/session` API endpoints.\\n"}}, "responses": {"ClientErrorResponse": {"description": "There is an error in the request.", "content": {"application/json": {"schema": {"properties": {"error": {"type": "string", "description": "A message describing the error.\\n"}}}}}}}}, "tags": [{"name": "apikeys", "description": "API Key APIs"}, {"name": "billing", "description": "Billing APIs"}, {"name": "logs", "description": "Logging APIs"}, {"name": "services", "description": "Services APIs"}], "security": [{"tokenAuth": []}]} """, ) @@ -199,7 +199,7 @@ def test_openapi_using_realm() -> None: page.ast, """ - + cloud diff --git a/snooty/test_postprocess.py b/snooty/test_postprocess.py index d0e43723..2456f092 100644 --- a/snooty/test_postprocess.py +++ b/snooty/test_postprocess.py @@ -2451,3 +2451,95 @@ def test_metadata() -> None: metadata = cast(Dict[str, Any], result.metadata) assert len(metadata["associated_products"]) == 1 assert len(metadata["associated_products"][0]["versions"]) == 2 + + +def test_openapi_metadata() -> None: + with make_test( + { + Path( + "source/admin/api/v3.txt" + ): """ +:orphan: +:template: openapi +:title: Atlas App Services Admin API + +.. default-domain: mongodb + +.. _admin-api: + +.. openapi:: /openapi-admin-v3.yaml + """, + Path("source/openapi-admin-v3.yaml"): "", + Path( + "source/admin/api/url.txt" + ): """ +.. openapi:: https://raw.githubusercontent.com/mongodb/snooty-parser/master/test_data/test_parser/openapi-admin-v3.yaml + """, + Path( + "source/admin/api/atlas.txt" + ): """ +.. openapi:: cloud + :uses-realm: + """, + } + ) as result: + assert not [ + diagnostics for diagnostics in result.diagnostics.values() if diagnostics + ], "Should not raise any diagnostics" + openapi_pages = cast(Dict[str, Any], result.metadata["openapi_pages"]) + + local_file_page = openapi_pages["admin/api/v3"] + assert local_file_page["source_type"] == "local" + assert local_file_page["source"] == "/openapi-admin-v3.yaml" + + url_page = openapi_pages["admin/api/url"] + assert url_page["source_type"] == "url" + assert ( + url_page["source"] + == "https://raw.githubusercontent.com/mongodb/snooty-parser/master/test_data/test_parser/openapi-admin-v3.yaml" + ) + + atlas_page = openapi_pages["admin/api/atlas"] + assert atlas_page["source_type"] == "atlas" + assert atlas_page["source"] == "cloud" + + +def test_openapi_preview() -> None: + with make_test( + { + Path( + "source/admin/api/preview.txt" + ): """ +.. openapi:: https://raw.githubusercontent.com/mongodb/snooty-parser/master/test_data/test_parser/openapi-admin-v3.yaml + :preview: + """, + } + ) as result: + assert not [ + diagnostics for diagnostics in result.diagnostics.values() if diagnostics + ], "Should not raise any diagnostics" + assert "openapi_pages" not in result.metadata + + +def test_openapi_duplicates() -> None: + with make_test( + { + Path( + "source/admin/api/v3.txt" + ): """ +.. openapi:: /openapi-admin-v3.yaml + +.. openapi:: https://raw.githubusercontent.com/mongodb/snooty-parser/master/test_data/test_parser/openapi-admin-v3.yaml + """, + Path("source/openapi-admin-v3.yaml"): "", + } + ) as result: + diagnostics = result.diagnostics[FileId("admin/api/v3.txt")] + assert len(diagnostics) == 1 + assert isinstance(diagnostics[0], DuplicateDirective) + + openapi_pages = cast(Dict[str, Any], result.metadata["openapi_pages"]) + # First openapi directive should be source of truth + file_metadata = openapi_pages["admin/api/v3"] + assert file_metadata["source_type"] == "local" + assert file_metadata["source"] == "/openapi-admin-v3.yaml"