10000 improve documentation parts api (#8573) · codeperl/localstack@fd4b059 · GitHub
[go: up one dir, main page]

Skip to content

Commit fd4b059

Browse files
authored
improve documentation parts api (localstack#8573)
1 parent 3480ef3 commit fd4b059

File tree

5 files changed

+255
-4
lines changed

5 files changed

+255
-4
lines changed

localstack/services/apigateway/helpers.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from localstack.aws.api.apigateway import (
2323
Authorizer,
2424
ConnectionType,
25+
DocumentationPart,
26+
DocumentationPartLocation,
2527
IntegrationType,
2628
Model,
2729
RequestValidator,
@@ -778,6 +780,25 @@ def apply_json_patch_safe(subject, patch_operations, in_place=True, return_list=
778780
return (results or [subject])[-1]
779781

780782

783+
def add_documentation_parts(rest_api_container, documentation):
784+
for doc_part in documentation.get("documentationParts", []):
785+
entity_id = short_uid()[:6]
786+
location = doc_part["location"]
787+
rest_api_container.documentation_parts[entity_id] = DocumentationPart(
788+
id=entity_id,
789+
location=DocumentationPartLocation(
790+
type=location.get("type"),
791+
path=location.get("path", "/")
792+
if location.get("type") not in ["API", "MODEL"]
793+
else None,
794+
method=location.get("method"),
795+
statusCode=location.get("statusCode"),
796+
name=location.get("name"),
797+
),
798+
properties=doc_part["properties"],
799+
)
800+
801+
781802
def import_api_from_openapi_spec(
782803
rest_api: RestAPI, body: Dict, query_params: Dict, account_id: str = None, region: str = None
783804
) -> Optional[RestAPI]:
@@ -1227,6 +1248,9 @@ def create_method_resource(child, method, method_schema):
12271248
if api_key_source is not None:
12281249
rest_api.api_key_source = api_key_source.upper()
12291250

1251+
documentation = resolved_schema.get(OpenAPIExt.DOCUMENTATION)
1252+
if documentation:
1253+
add_documentation_parts(rest_api_container, documentation)
12301254
return rest_api
12311255

12321256

localstack/services/apigateway/provider.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
CreateAuthorizerRequest,
3232
CreateRestApiRequest,
3333
DocumentationPart,
34+
DocumentationPartIds,
3435
DocumentationPartLocation,
3536
DocumentationParts,
3637
ExportResponse,
@@ -51,6 +52,7 @@
5152
NullableInteger,
5253
PutIntegrationRequest,
5354
PutIntegrationResponseRequest,
55+
PutMode,
5456
PutRestApiRequest,
5557
RequestValidator,
5658
RequestValidators,
@@ -71,13 +73,15 @@
7173
EMPTY_MODEL,
7274
ERROR_MODEL,
7375
OpenApiExporter,
76+
OpenAPIExt,
7477
apply_json_patch_safe,
7578
get_apigateway_store,
7679
import_api_from_openapi_spec,
7780
is_greedy_path,
7881
is_variable_path,
7982
log_template,
8083
multi_value_dict_for_list,
84+
resolve_references,
8185
)
8286
from localstack.services.apigateway.invocations import invoke_rest_api_from_request
8387
from localstack.services.apigateway.models import RestApiContainer
@@ -945,6 +949,41 @@ def delete_documentation_part(
945949
if rest_api_container:
946950
rest_api_container.documentation_parts.pop(documentation_part_id, None)
947951

952+
def import_documentation_parts(
953+
self,
954+
context: RequestContext,
955+
rest_api_id: String,
956+
body: IO[Blob],
957+
mode: PutMode = None,
958+
fail_on_warnings: Boolean = None,
959+
) -> DocumentationPartIds:
960+
961+
body_data = body.read()
962+
openapi_spec = parse_json_or_yaml(to_str(body_data))
963+
964+
store = get_apigateway_store(account_id=context.account_id, region=context.region)
965+
if not (rest_api_container := store.rest_apis.get(rest_api_id)):
966+
raise NotFoundException(
967+
f"Invalid API identifier specified {context.account_id}:{rest_api_id}"
968+
)
969+
970+
# https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-documenting-api-quick-start-import-export.html
971+
resolved_schema = resolve_references(openapi_spec, rest_api_id=rest_api_id)
972+
documentation = resolved_schema.get(OpenAPIExt.DOCUMENTATION)
973+
974+
ids = []
975+
# overwrite mode
976+
if mode == PutMode.overwrite:
977+
rest_api_container.documentation_parts.clear()
978+
for doc_part in documentation["documentationParts"]:
979+
entity_id = short_uid()[:6]
980+
rest_api_container.documentation_parts[entity_id] = DocumentationPart(
981+
id=entity_id, **doc_part
982+
)
983+
ids.append(entity_id)
984+
# TODO: implement the merge mode
985+
return DocumentationPartIds(ids=ids)
986+
948987
# base path mappings
949988

950989
def get_base_path_mappings(
@@ -1642,14 +1681,13 @@ def delete_model(
16421681

16431682
def get_moto_rest_api(context: RequestContext, rest_api_id: str) -> MotoRestAPI:
16441683
moto_backend = apigw_models.apigateway_backends[context.account_id][context.region]
1645-
rest_api = moto_backend.apis.get(rest_api_id)
1646-
if not rest_api:
1684+
if rest_api := moto_backend.apis.get(rest_api_id):
1685+
return rest_api
1686+
else:
16471687
raise NotFoundException(
16481688
f"Invalid API identifier specified {context.account_id}:{rest_api_id}"
16491689
)
16501690

1651-
return rest_api
1652-
16531691

16541692
def remove_empty_attributes_from_rest_api(rest_api: RestApi, remove_tags=True) -> RestApi:
16551693
if not rest_api.get("binaryMediaTypes"):

tests/integration/apigateway/test_apigateway_api.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
import json
22
import logging
3+
import os.path
34
import time
45
from operator import itemgetter
56

67
import pytest
78
from botocore.exceptions import ClientError
89

10+
from localstack.aws.api.apigateway import PutMode
911
from localstack.services.apigateway.helpers import TAG_KEY_CUSTOM_ID
1012
from localstack.testing.aws.util import is_aws_cloud
1113
from localstack.testing.snapshots.transformer import KeyValueBasedTransformer, SortingTransformer
14+
from localstack.utils.files import load_file
1215
from localstack.utils.strings import short_uid
1316
from localstack.utils.sync import retry
1417
from tests.integration.apigateway.apigateway_fixtures import (
@@ -21,6 +24,9 @@
2124

2225
LOG = logging.getLogger(__name__)
2326

27+
PARENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
28+
OAS_30_DOCUMENTATION_PARTS = os.path.join(PARENT_DIR, "files", "oas30_documentation_parts.json")
29+
2430

2531
@pytest.fixture(autouse=True)
2632
def apigw_snapshot_transformer(snapshot):
@@ -2049,3 +2055,36 @@ def test_invalid_delete_documentation_part(self, apigw_create_rest_api, snapshot
20492055
documentationPartId=documentation_part_id,
20502056
)
20512057
snapshot.match("delete_already_deleted_documentation_part", e.value.response)
2058+
2059+
@pytest.mark.aws_validated
2060+
def test_import_documentation_parts(self, aws_client, import_apigw, snapshot):
2061+
# snapshot array "ids"
2062+
snapshot.add_transformer(snapshot.transform.jsonpath("$..ids[*]", "id"))
2063+
# create api with documentation imports
2064+
spec_file = load_file(OAS_30_DOCUMENTATION_PARTS)
2065+
response, root_id = import_apigw(body=spec_file, failOnWarnings=True)
2066+
rest_api_id = response["id"]
2067+
2068+
# get documentation parts to make sure import worked
2069+
response = aws_client.apigateway.get_documentation_parts(restApiId=rest_api_id)
2070+
snapshot.match("create-import-documentations_parts", response["items"])
2071+
2072+
# delete documentation parts
2073+
for doc_part_item in response["items"]:
2074+
response = aws_client.apigateway.delete_documentation_part(
2075+
restApiId=rest_api_id,
2076+
documentationPartId=doc_part_item["id"],
2077+
)
2078+
assert response["ResponseMetadata"]["HTTPStatusCode"] == 202
2079+
2080+
# make sure delete parts are gone
2081+
response = aws_client.apigateway.get_documentation_parts(restApiId=rest_api_id)
2082+
assert len(response["items"]) == 0
2083+
2084+
# import documentation parts using import documentation parts api
2085+
response = aws_client.apigateway.import_documentation_parts(
2086+
restApiId=rest_api_id,
2087+
mode=PutMode.overwrite,
2088+
body=spec_file,
2089+
)
2090+
snapshot.match("import-documentation-parts", response)

tests/integration/apigateway/test_apigateway_api.snapshot.json

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2538,5 +2538,70 @@
25382538
}
25392539
}
25402540
}
2541+
},
2542+
"tests/integration/apigateway/test_apigateway_api.py::TestApiGatewayApiDocumentationPart::test_import_documentation_parts": {
2543+
"recorded-date": "26-06-2023, 12:01:38",
2544+
"recorded-content": {
2545+
"create-import-documentations_parts": [
2546+
{
2547+
"id": "<id:1>",
2548+
"location": {
2549+
"type": "API"
2550+
},
2551+
"properties": {
2552+
"description": "API description",
2553+
"info": {
2554+
"description": "API info description 4",
2555+
"version": "API info version 3"
2556+
}
2557+
}
2558+
},
2559+
{
2560+
"id": "<id:2>",
2561+
"location": {
2562+
"type": "METHOD",
2563+
"path": "/",
2564+
"method": "GET"
2565+
},
2566+
"properties": {
2567+
"description": "Method description."
2568+
}
2569+
},
2570+
{
2571+
"id": "<id:3>",
2572+
"location": {
2573+
"type": "MODEL",
2574+
"name": "<name:1>"
2575+
},
2576+
"properties": {
2577+
"title": "<name:1> Schema"
2578+
}
2579+
},
2580+
{
2581+
"id": "<id:4>",
2582+
"location": {
2583+
"type": "RESPONSE",
2584+
"path": "/",
2585+
"method": "GET",
2586+
"statusCode": "200"
2587+
},
2588+
"properties": {
2589+
"description": "200 response"
2590+
}
2591+
}
2592+
],
2593+
"import-documentation-parts": {
2594+
"ids": [
2595+
"<id:5>",
2596+
"<id:6>",
2597+
"<id:7>",
2598+
"<id:8>"
2599+
],
2600+
"ResponseMetadata": {
2601+
"HTTPHeaders": {},
2602+
"HTTPStatusCode": 200
2603+
}
2604+
}
2605+
}
25412606
}
25422607
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
{
2+
"openapi": "3.0.0",
3+
"info": {
4+
"description": "description",
5+
"version": "1",
6+
"title": "doc"
7+
},
8+
"paths": {
9+
"/": {
10+
"get": {
11+
"description": "Method description.",
12+
"responses": {
13+
"200": {
14+
"description": "200 response",
15+
"content": {
16+
"application/json": {
17+
"schema": {
18+
"$ref": "#/components/schemas/Empty"
19+
}
20+
}
21+
}
22+
}
23+
}
24+
}
25+
}
26+
},
27+
"x-amazon-apigateway-documentation": {
28+
"version": "1.0.3",
29+
"documentationParts": [
30+
{
31+
"location": {
32+
"type": "API"
33+
},
34+
"properties": {
35+
"description": "API description",
36+
"info": {
37+
"description": "API info description 4",
38+
"version": "API info version 3"
39+
}
40+
}
41+
},
42+
{
43+
"location": {
44+
"type": "METHOD",
45+
"method": "GET"
46+
},
47+
"properties": {
48+
"description": "Method description."
49+
}
50+
},
51+
{
52+
"location": {
53+
"type": "MODEL",
54+
"name": "Empty"
55+
},
56+
"properties": {
57+
"title": "Empty Schema"
58+
}
59+
},
60+
{
61+
"location": {
62+
"type": "RESPONSE",
63+
"method": "GET",
64+
"statusCode": "200"
65+
},
66+
"properties": {
67+
"description": "200 response"
68+
}
69+
}
70+
]
71+
},
72+
"servers": [
73+
{
74+
"url": "/"
75+
}
76+
],
77+
"components": {
78+
"schemas": {
79+
"Empty": {
80+
"type": "object",
81+
"title": "Empty Schema"
82+
}
83+
}
84+
}
85+
}

0 commit comments

Comments
 (0)
0