Skip to content

Commit 81d357a

Browse files
Improve model deserialization (#18)
* improve model deserialization * move away from es6 map * update dependencies for class transformer * increase test coverage * modify primitive callable type * simplify typescript template * update generics within progress event class
1 parent 4399cca commit 81d357a

27 files changed

+1237
-775
lines changed

global.d.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ module.exports = {
1616
coverageDirectory: 'coverage/ts',
1717
collectCoverage: true,
1818
coverageReporters: ['json', 'lcov', 'text'],
19+
coveragePathIgnorePatterns: ['/node_modules/', '/tests/data/'],
1920
};

package-lock.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"test:debug": "npx --node-arg=--inspect jest --runInBand"
2020
},
2121
"engines": {
22-
"node": ">=10.0.0",
22+
"node": ">=10.4.0",
2323
"npm": ">=5.6.0"
2424
},
2525
"repository": {
@@ -34,6 +34,7 @@
3434
"homepage": "https://github.com/eduardomourar/cloudformation-cli-typescript-plugin#readme",
3535
"dependencies": {
3636
"autobind-decorator": "^2.4.0",
37+
"class-transformer": "^0.3.1",
3738
"promise-sequential": "^1.1.1",
3839
"reflect-metadata": "^0.1.13",
3940
"tombok": "https://github.com/eduardomourar/tombok/releases/download/v0.0.1/tombok-0.0.1.tgz",

python/rpdk/typescript/codegen.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from rpdk.core.jsonutils.resolver import ContainerType, resolve_models
1111
from rpdk.core.plugin_base import LanguagePlugin
1212

13-
from .resolver import contains_model, translate_type
13+
from .resolver import contains_model, get_inner_type, translate_type
1414
from .utils import safe_reserved
1515

1616
LOG = logging.getLogger(__name__)
@@ -42,6 +42,7 @@ def __init__(self):
4242
)
4343
self.env.filters["translate_type"] = translate_type
4444
self.env.filters["contains_model"] = contains_model
45+
self.env.filters["get_inner_type"] = get_inner_type
4546
self.env.filters["safe_reserved"] = safe_reserved
4647
self.env.globals["ContainerType"] = ContainerType
4748
self.namespace = None
@@ -159,7 +160,11 @@ def generate(self, project):
159160
LOG.debug("Writing file: %s", path)
160161
template = self.env.get_template("models.ts")
161162
contents = template.render(
162-
lib_name=SUPPORT_LIB_NAME, type_name=project.type_name, models=models,
163+
lib_name=SUPPORT_LIB_NAME,
164+
type_name=project.type_name,
165+
models=models,
166+
primaryIdentifier=project.schema.get("primaryIdentifier", []),
167+
additionalIdentifiers=project.schema.get("additionalIdentifiers", []),
163168
)
164169
project.overwrite(path, contents)
165170

python/rpdk/typescript/resolver.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,61 @@
22

33
PRIMITIVE_TYPES = {
44
"string": "string",
5-
"integer": "number",
5+
"integer": "integer",
66
"boolean": "boolean",
77
"number": "number",
8-
UNDEFINED: "Object",
8+
UNDEFINED: "object",
99
}
10+
PRIMITIVE_WRAPPERS = {
11+
"string": "String",
12+
"integer": "Integer",
13+
"boolean": "Boolean",
14+
"number": "Number",
15+
"object": "Object",
16+
}
17+
18+
19+
class InnerType:
20+
def __init__(self, item_type):
21+
self.primitive = False
22+
self.classes = []
23+
self.type = self.resolve_type(item_type)
24+
self.wrapper_type = self.type
25+
if self.primitive:
26+
self.wrapper_type = PRIMITIVE_WRAPPERS[self.type]
27+
28+
def resolve_type(self, resolved_type):
29+
if resolved_type.container == ContainerType.PRIMITIVE:
30+
self.primitive = True
31+
return PRIMITIVE_TYPES[resolved_type.type]
32+
if resolved_type.container == ContainerType.MULTIPLE:
33+
self.primitive = True
34+
return "object"
35+
if resolved_type.container == ContainerType.MODEL:
36+
return resolved_type.type
37+
if resolved_type.container == ContainerType.DICT:
38+
self.classes.append("Map")
39+
elif resolved_type.container == ContainerType.LIST:
40+
self.classes.append("Array")
41+
elif resolved_type.container == ContainerType.SET:
42+
self.classes.append("Set")
43+
else:
44+
raise ValueError(f"Unknown container type {resolved_type.container}")
45+
46+
return self.resolve_type(resolved_type.type)
47+
48+
49+
def get_inner_type(resolved_type):
50+
return InnerType(resolved_type)
1051

1152

1253
def translate_type(resolved_type):
1354
if resolved_type.container == ContainerType.MODEL:
1455
return resolved_type.type
1556
if resolved_type.container == ContainerType.PRIMITIVE:
1657
return PRIMITIVE_TYPES[resolved_type.type]
58+
if resolved_type.container == ContainerType.MULTIPLE:
59+
return "object"
1760

1861
item_type = translate_type(resolved_type.type)
1962

python/rpdk/typescript/templates/handlers.ts

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { ResourceModel } from './models';
1515
// Use this logger to forward log messages to CloudWatch Logs.
1616
const LOGGER = console;
1717

18+
interface CallbackContext extends Record<string, any> {}
19+
1820
class Resource extends BaseResource<ResourceModel> {
1921

2022
/**
@@ -23,19 +25,17 @@ class Resource extends BaseResource<ResourceModel> {
2325
*
2426
* @param session Current AWS session passed through from caller
2527
* @param request The request object for the provisioning request passed to the implementor
26-
* @param callbackContext Custom context object to enable handlers to process re-invocation
28+
* @param callbackContext Custom context object to allow the passing through of additional
29+
* state or metadata between subsequent retries
2730
*/
2831
@handlerEvent(Action.Create)
2932
public async create(
3033
session: Optional<SessionProxy>,
3134
request: ResourceHandlerRequest<ResourceModel>,
32-
callbackContext: Map<string, any>,
35+
callbackContext: CallbackContext,
3336
): Promise<ProgressEvent> {
3437
const model: ResourceModel = request.desiredResourceState;
35-
const progress: ProgressEvent<ResourceModel> = ProgressEvent.builder()
36-
.status(OperationStatus.InProgress)
37-
.resourceModel(model)
38-
.build() as ProgressEvent<ResourceModel>;
38+
const progress = ProgressEvent.progress<ProgressEvent<ResourceModel, CallbackContext>>(model);
3939
// TODO: put code here
4040

4141
// Example:
@@ -61,19 +61,17 @@ class Resource extends BaseResource<ResourceModel> {
6161
*
6262
* @param session Current AWS session passed through from caller
6363
* @param request The request object for the provisioning request passed to the implementor
64-
* @param callbackContext Custom context object to enable handlers to process re-invocation
64+
* @param callbackContext Custom context object to allow the passing through of additional
65+
* state or metadata between subsequent retries
6566
*/
6667
@handlerEvent(Action.Update)
6768
public async update(
6869
session: Optional<SessionProxy>,
6970
request: ResourceHandlerRequest<ResourceModel>,
70-
callbackContext: Map<string, any>,
71+
callbackContext: CallbackContext,
7172
): Promise<ProgressEvent> {
7273
const model: ResourceModel = request.desiredResourceState;
73-
const progress: ProgressEvent<ResourceModel> = ProgressEvent.builder()
74-
.status(OperationStatus.InProgress)
75-
.resourceModel(model)
76-
.build() as ProgressEvent<ResourceModel>;
74+
const progress = ProgressEvent.progress<ProgressEvent<ResourceModel, CallbackContext>>(model);
7775
// TODO: put code here
7876
progress.status = OperationStatus.Success;
7977
return progress;
@@ -86,18 +84,17 @@ class Resource extends BaseResource<ResourceModel> {
8684
*
8785
* @param session Current AWS session passed through from caller
8886
* @param request The request object for the provisioning request passed to the implementor
89-
* @param callbackContext Custom context object to enable handlers to process re-invocation
87+
* @param callbackContext Custom context object to allow the passing through of additional
88+
* state or metadata between subsequent retries
9089
*/
9190
@handlerEvent(Action.Delete)
9291
public async delete(
9392
session: Optional<SessionProxy>,
9493
request: ResourceHandlerRequest<ResourceModel>,
95-
callbackContext: Map<string, any>,
94+
callbackContext: CallbackContext,
9695
): Promise<ProgressEvent> {
9796
const model: ResourceModel = request.desiredResourceState;
98-
const progress: ProgressEvent<ResourceModel> = ProgressEvent.builder()
99-
.status(OperationStatus.InProgress)
100-
.build() as ProgressEvent<ResourceModel>;
97+
const progress = ProgressEvent.progress<ProgressEvent<ResourceModel, CallbackContext>>();
10198
// TODO: put code here
10299
progress.status = OperationStatus.Success;
103100
return progress;
@@ -109,20 +106,18 @@ class Resource extends BaseResource<ResourceModel> {
109106
*
110107
* @param session Current AWS session passed through from caller
111108
* @param request The request object for the provisioning request passed to the implementor
112-
* @param callbackContext Custom context object to enable handlers to process re-invocation
109+
* @param callbackContext Custom context object to allow the passing through of additional
110+
* state or metadata between subsequent retries
113111
*/
114112
@handlerEvent(Action.Read)
115113
public async read(
116114
session: Optional<SessionProxy>,
117115
request: ResourceHandlerRequest<ResourceModel>,
118-
callbackContext: Map<string, any>,
116+
callbackContext: CallbackContext,
119117
): Promise<ProgressEvent> {
120118
const model: ResourceModel = request.desiredResourceState;
121119
// TODO: put code here
122-
const progress: ProgressEvent<ResourceModel> = ProgressEvent.builder()
123-
.status(OperationStatus.Success)
124-
.resourceModel(model)
125-
.build() as ProgressEvent<ResourceModel>;
120+
const progress = ProgressEvent.success<ProgressEvent<ResourceModel, CallbackContext>>(model);
126121
return progress;
127122
}
128123

@@ -132,19 +127,21 @@ class Resource extends BaseResource<ResourceModel> {
132127
*
133128
* @param session Current AWS session passed through from caller
134129
* @param request The request object for the provisioning request passed to the implementor
135-
* @param callbackContext Custom context object to enable handlers to process re-invocation
130+
* @param callbackContext Custom context object to allow the passing through of additional
131+
* state or metadata between subsequent retries
136132
*/
137133
@handlerEvent(Action.List)
138134
public async list(
139135
session: Optional<SessionProxy>,
140136
request: ResourceHandlerRequest<ResourceModel>,
141-
callbackContext: Map<string, any>,
137+
callbackContext: CallbackContext,
142138
): Promise<ProgressEvent> {
139+
const model: ResourceModel = request.desiredResourceState;
143140
// TODO: put code here
144-
const progress: ProgressEvent<ResourceModel> = ProgressEvent.builder()
141+
const progress = ProgressEvent.builder<ProgressEvent<ResourceModel, CallbackContext>>()
145142
.status(OperationStatus.Success)
146-
.resourceModels([])
147-
.build() as ProgressEvent<ResourceModel>;
143+
.resourceModels([model])
144+
.build();
148145
return progress;
149146
}
150147
}
Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,103 @@
11
// This is a generated file. Modifications will be overwritten.
2-
import { BaseModel, Optional } from '{{lib_name}}';
2+
import { BaseModel, Dict, integer, Integer, Optional, transformValue } from '{{lib_name}}';
3+
import { Exclude, Expose, Type, Transform } from 'class-transformer';
34

45
{% for model, properties in models.items() %}
5-
export class {{ model|uppercase_first_letter }}{% if model == "ResourceModel" %} extends BaseModel{% endif %} {
6+
export class {{ model|uppercase_first_letter }} extends BaseModel {
67
['constructor']: typeof {{ model|uppercase_first_letter }};
8+
9+
{% if model == "ResourceModel" %}
10+
@Exclude()
711
public static readonly TYPE_NAME: string = '{{ type_name }}';
812

13+
{% for identifier in primaryIdentifier %}
14+
{% set components = identifier.split("/") %}
15+
@Exclude()
16+
protected readonly IDENTIFIER_KEY_{{ components[2:]|join('_')|upper }}: string = '{{ identifier }}';
17+
{% endfor -%}
18+
19+
{% for identifiers in additionalIdentifiers %}
20+
{% for identifier in identifiers %}
21+
{% set components = identifier.split("/") %}
22+
@Exclude()
23+
protected readonly IDENTIFIER_KEY_{{ components[2:]|join('_')|upper }}: string = '{{ identifier }}';
24+
{% endfor %}
25+
{% endfor %}
26+
{% endif %}
27+
928
{% for name, type in properties.items() %}
10-
{{ name|safe_reserved }}: Optional<{{ type|translate_type }}>;
29+
{% set translated_type = type|translate_type %}
30+
{% set inner_type = type|get_inner_type %}
31+
@Expose({ name: '{{ name }}' })
32+
{% if type|contains_model %}
33+
@Type(() => {{ inner_type.type }})
34+
{% else %}
35+
@Transform(
36+
(value: any, obj: any) =>
37+
transformValue({{ inner_type.wrapper_type }}, '{{ name|lowercase_first_letter|safe_reserved }}', value, obj, [{{ inner_type.classes|join(', ') }}]),
38+
{
39+
toClassOnly: true,
40+
}
41+
)
42+
{% endif %}
43+
{{ name|lowercase_first_letter|safe_reserved }}?: Optional<{{ translated_type }}>;
44+
{% endfor %}
45+
46+
{% if model == "ResourceModel" %}
47+
@Exclude()
48+
public getPrimaryIdentifier(): Dict {
49+
const identifier: Dict = {};
50+
{% for identifier in primaryIdentifier %}
51+
{% set components = identifier.split("/") %}
52+
if (this.{{components[2]|lowercase_first_letter}} != null
53+
{%- for i in range(4, components|length + 1) -%}
54+
{#- #} && this
55+
{%- for component in components[2:i] -%} .{{component|lowercase_first_letter}} {%- endfor -%}
56+
{#- #} != null
57+
{%- endfor -%}
58+
) {
59+
identifier[this.IDENTIFIER_KEY_{{ components[2:]|join('_')|upper }}] = this{% for component in components[2:] %}.{{component|lowercase_first_letter}}{% endfor %};
60+
}
61+
62+
{% endfor %}
63+
// only return the identifier if it can be used, i.e. if all components are present
64+
return Object.keys(identifier).length === {{ primaryIdentifier|length }} ? identifier : null;
65+
}
66+
67+
@Exclude()
68+
public getAdditionalIdentifiers(): Array<Dict> {
69+
const identifiers: Array<Dict> = new Array<Dict>();
70+
{% for identifiers in additionalIdentifiers %}
71+
if (this.getIdentifier {%- for identifier in identifiers -%} _{{identifier.split("/")[-1]|uppercase_first_letter}} {%- endfor -%} () != null) {
72+
identifiers.push(this.getIdentifier{% for identifier in identifiers %}_{{identifier.split("/")[-1]|uppercase_first_letter}}{% endfor %}());
73+
}
74+
{% endfor %}
75+
// only return the identifiers if any can be used
76+
return identifiers.length === 0 ? null : identifiers;
77+
}
78+
{% for identifiers in additionalIdentifiers %}
79+
80+
@Exclude()
81+
public getIdentifier {%- for identifier in identifiers -%} _{{identifier.split("/")[-1]|uppercase_first_letter}} {%- endfor -%} (): Dict {
82+
const identifier: Dict = {};
83+
{% for identifier in identifiers %}
84+
{% set components = identifier.split("/") %}
85+
if ((this as any).{{components[2]|lowercase_first_letter}} != null
86+
{%- for i in range(4, components|length + 1) -%}
87+
{#- #} && (this as any)
88+
{%- for component in components[2:i] -%} .{{component|lowercase_first_letter}} {%- endfor -%}
89+
{#- #} != null
90+
{%- endfor -%}
91+
) {
92+
identifier[this.IDENTIFIER_KEY_{{ components[2:]|join('_')|upper }}] = (this as any){% for component in components[2:] %}.{{component|lowercase_first_letter}}{% endfor %};
93+
}
94+
95+
{% endfor %}
96+
// only return the identifier if it can be used, i.e. if all components are present
97+
return Object.keys(identifier).length === {{ identifiers|length }} ? identifier : null;
98+
}
1199
{% endfor %}
100+
{% endif %}
12101
}
13102

14103
{% endfor -%}

python/rpdk/typescript/templates/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"test": "echo \"Error: no test specified\" && exit 1"
1313
},
1414
"dependencies": {
15-
"{{lib_name}}": "{{lib_path}}"
15+
"{{lib_name}}": "{{lib_path}}",
16+
"class-transformer": "^0.3.1"
1617
},
1718
"devDependencies": {
1819
"@types/node": "^12.0.0",

0 commit comments

Comments
 (0)