Skip to content

Commit 60d4b16

Browse files
committed
The start of server side completion
Uses boto3 resources to generate server side completion.
1 parent a041766 commit 60d4b16

File tree

3 files changed

+220
-0
lines changed

3 files changed

+220
-0
lines changed

awsshell/resource/__init__.py

Whitespace-only changes.

awsshell/resource/index.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Index and retrive information from the resource JSON."""
2+
import jmespath
3+
4+
5+
def extract_field_from_jmespath(expression):
6+
result = jmespath.compile(expression)
7+
current = result.parsed
8+
while current['children']:
9+
current = current['children'][0]
10+
if current['type'] == 'field':
11+
return current['value']
12+
13+
14+
class ResourceIndexBuilder(object):
15+
def __init__(self):
16+
pass
17+
18+
def build_index(self, resource_data):
19+
# First we need to go through the 'resources'
20+
# key and map all of its actions back to the
21+
# resource name.
22+
index = {
23+
'operations': {},
24+
'resources': {},
25+
}
26+
service = resource_data['service']
27+
if 'hasMany' in service:
28+
for has_many_name, model in service['hasMany'].items():
29+
resource_name = model['resource']['type']
30+
for identifier in model['resource']['identifiers']:
31+
first_identifier = model['resource']['identifiers'][0]
32+
index['resources'][resource_name] = {
33+
'operation': model['request']['operation'],
34+
# TODO: map all the identifiers.
35+
# We're only taking the first one for now.
36+
'resourceIdentifier': {
37+
first_identifier['target']: first_identifier['path']
38+
}
39+
}
40+
for resource_name, model in resource_data['resources'].items():
41+
if resource_name not in index['resources']:
42+
continue
43+
if 'actions' in model:
44+
resource_actions = model['actions']
45+
for action_name, action_model in resource_actions.items():
46+
op_name = action_model['request']['operation']
47+
current = {}
48+
index['operations'][op_name] = current
49+
for param in action_model['request']['params']:
50+
if param['source'] == 'identifier':
51+
field_name = extract_field_from_jmespath(
52+
param['target'])
53+
current[field_name] = {
54+
'resourceName': resource_name,
55+
'resourceIdentifier': param['name'],
56+
}
57+
return index
58+
59+
60+
def main():
61+
import sys
62+
import json
63+
builder = ResourceIndexBuilder()
64+
index = builder.build_index(json.load(open(sys.argv[1])))
65+
print json.dumps(index, indent=2)
66+
67+
68+
if __name__ == '__main__':
69+
main()

tests/test_resources.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
"""Index and retrive information from the resource JSON."""
2+
import pytest
3+
from awsshell.resource import index
4+
5+
6+
def test_build_from_has_many():
7+
resource = {
8+
'service': {
9+
'hasMany': {
10+
'Tables': {
11+
'request': {'operation': 'ListTables'},
12+
'resource': {
13+
'type': 'Table',
14+
'identifiers': [
15+
{'target': 'Name',
16+
'source': 'response',
17+
'path': 'TableNames[]',
18+
}
19+
]
20+
}
21+
}
22+
}
23+
},
24+
'resources': {
25+
'Table': {
26+
'actions': {
27+
'Delete': {
28+
'request': {
29+
'operation': 'DeleteTable',
30+
'params': [
31+
{'target': 'TableName',
32+
'source': 'identifier',
33+
'name': 'Name'},
34+
]
35+
}
36+
}
37+
}
38+
}
39+
}
40+
}
41+
builder = index.ResourceIndexBuilder()
42+
built_index = builder.build_index(resource)
43+
assert built_index == {
44+
'operations': {
45+
'DeleteTable': {
46+
'TableName': {
47+
'resourceName': 'Table',
48+
'resourceIdentifier': 'Name',
49+
}
50+
}
51+
},
52+
'resources': {
53+
'Table': {
54+
'operation': 'ListTables',
55+
'resourceIdentifier': {
56+
'Name': 'TableNames[]',
57+
}
58+
}
59+
}
60+
}
61+
62+
63+
def test_removes_jmespath_expressions_from_targets():
64+
resource = {
65+
'service': {
66+
'hasMany': {
67+
'Instances': {
68+
'request': {'operation': 'DescribeInstances'},
69+
'resource': {
70+
'type': 'Instance',
71+
'identifiers': [
72+
{'target': 'Id',
73+
'source': 'response',
74+
'path': 'Reservations[].Instances[].InstanceId',
75+
}
76+
]
77+
}
78+
}
79+
}
80+
},
81+
'resources': {
82+
'Instance': {
83+
'actions': {
84+
'Terminate': {
85+
'request': {
86+
'operation': 'TerminateInstances',
87+
'params': [
88+
{'target': 'InstanceIds[0]',
89+
'source': 'identifier',
90+
'name': 'Id'},
91+
]
92+
}
93+
}
94+
}
95+
}
96+
}
97+
}
98+
builder = index.ResourceIndexBuilder()
99+
built_index = builder.build_index(resource)
100+
assert built_index == {
101+
'operations': {
102+
'TerminateInstances': {
103+
'InstanceIds': {
104+
'resourceName': 'Instance',
105+
'resourceIdentifier': 'Id',
106+
}
107+
}
108+
},
109+
'resources': {
110+
'Instance': {
111+
'operation': 'DescribeInstances',
112+
'resourceIdentifier': {
113+
'Id': 'Reservations[].Instances[].InstanceId',
114+
}
115+
}
116+
}
117+
}
118+
119+
120+
def test_resource_not_included_if_no_has_many():
121+
# This is something we can fix, but for now the resource
122+
# must be in the hasMany.
123+
resource = {
124+
'service': {
125+
'hasMany': {}
126+
},
127+
'resources': {
128+
'Tag': {
129+
'actions': {
130+
'Delete': {
131+
'request': {
132+
'operation': 'DeleteTags',
133+
'params': [
134+
{'target': 'Resources[0]',
135+
'source': 'identifier',
136+
'name': 'ResourceId'},
137+
]
138+
}
139+
}
140+
}
141+
}
142+
}
143+
}
144+
builder = index.ResourceIndexBuilder()
145+
built_index = builder.build_index(resource)
146+
# The index is empty because there was not matching
147+
# hasMany resource.
148+
assert built_index == {
149+
'operations': {},
150+
'resources': {},
151+
}

0 commit comments

Comments
 (0)