-
Notifications
You must be signed in to change notification settings - Fork 4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix(cfn-include): fails to load SAM resources #9442
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Man this code is complicated. Both the task it's trying to achieve, but also in the implementation.
I trust that it does what it needs to do. Got some notes on potential cleanup strategies, do with those as you will.
const array1 = val1 == null ? [] : (Array.isArray(val1) ? val1 : [val1]); | ||
const array2 = val2 == null ? [] : (Array.isArray(val2) ? val2 : [val2]); | ||
for (const value of array2) { | ||
if (!array1.includes(value)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accidentally quadratic here. Not sure the set ever grows big enough for it to be an issue, but hope you thought about this and weighed the risk intentionally.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. I don't think this will ever realistically contain more than 2 elements.
tools/cfn2ts/lib/codegen.ts
Outdated
public visitAtomUnion(types: genspec.CodeName[]): string { | ||
const validatorNames = types.map(type => genspec.validatorName(type).fqn).join(', '); | ||
|
||
const arg = `prop${this.depth}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you made a function that returns unique variable names (and honestly, it could be global, why not?) you could reduce on the complexity of this.deeperCopy()
and below the 2 deeperCopy()
s.
In this generated code, there's no requirement that we use the least amount of variable names possible.
Just put this somewhere in this file and use it:
const freshVariable = (() => {
let ctr = 1;
return () => `var${ctr++}`;
}());
If you really wanted to be fancy you could make it scoped and reduce the counter again after the block to really end up with minimal variable names:
let ctr = 0;
function withFreshVariable<A>(block: (var: string) => A) {
ctr += 1;
const ret = block(`var${ctr}`);
ctr -= 1;
return ret;
}
// To be used as:
return withFreshVariable(var1 => withFreshVariable(var2 => `const ${var2} = ${var1}`));
// Or even:
function renderLambda(block: (var: string) => string) {
return withFreshVariable((var) => `(${var}: any) => ${block(var)}`);
}
// To be used as:
return renderLambda(v => `${FromCfnVisitor.fromExpression(v).visitAtom(itemType)}`);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried your withFreshVariable
refactoring, but it doesn't really do what you want in this case:
const arg1 = `prop${this.depth}`;
const arg2 = `prop${this.depth + 1}`;
const mappers = itemTypes
// we need a copy 2 levels deep, as we're already using prop2 in arg2 here
.map(type => `(${arg2}: any) => ${this.deeperCopy(arg1).deeperCopy(arg2).visitAtom(type)}`)
.join(', ');
return `${CFN_PARSE}.FromCloudFormation.getArray(${this.baseExpression}, ` +
`(${arg1}: any) => ${CFN_PARSE}.FromCloudFormation.getTypeUnion([${validatorNames}], [${mappers}], ${arg1})` +
')';
withFreshVariable
does nothing for us here (they would have to be nested calls).
I did a small refactoring of the logic though, and I think it's much more clear now. Let me know how you like it compared to the old one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
withFreshVariable
does nothing for us here (they would have to be nested calls).
Yes, they have to be nested calls. That's why it takes a block and returns the block's return value. Sorry if that wasn't clear.
tools/cfn2ts/lib/codegen.ts
Outdated
const arg2 = `prop${this.depth + 1}`; | ||
const mappers = itemTypes | ||
// we need a copy 2 levels deep, as we're already using prop2 in arg2 here | ||
.map(type => `(${arg2}: any) => ${this.deeperCopy(arg1).deeperCopy(arg2).visitAtom(type)}`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had to spent a solid 20 minutes perusing the code to see why you needed 2 copies here. Let's not? :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me know if it's clearer why that's needed in this new version.
@@ -117,6 +118,18 @@ export class FromCloudFormation { | |||
value: tag.Value, | |||
}; | |||
} | |||
|
|||
public static getTypeUnion(validators: Validator[], mappers: Mapper[], value: any): any { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feels like this function would be better off taking an array of pairs, rather than a pair of arrays?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I modeled this after the existing unionMapper
function in runtime.ts
.
Making it an array of pairs would make it much more difficult to write code like this:
const validatorNames = types.map(type => genspec.validatorName(type).fqn).join(', ');
const arg = `prop${this.depth}`;
const mappers = types
.map(type => `(${arg}: any) => ${this.deeperCopy(arg).visitAtom(type)}`)
.join(', ');
return `${CFN_PARSE}.FromCloudFormation.getTypeUnion([${validatorNames}], [${mappers}], ${this.baseExpression})`;
With the current modeling, you can easily generate separate validators and mappers. Making getTypeUnion ()
take an array of pairs would make this a fair bit more complicated.
tools/cfn2ts/lib/codegen.ts
Outdated
const scalarValidator = `${CORE}.unionValidator(${scalarValidatorNames})`; | ||
const listValidator = `${CORE}.listValidator(${CORE}.unionValidator(${itemValidatorNames}))`; | ||
|
||
return `${CFN_PARSE}.FromCloudFormation.getTypeUnion([${scalarValidator}, ${listValidator}], [${scalarMapper}, ${listMapper}], ${this.baseExpression})`; | ||
} | ||
|
||
private deeperCopy(baseExpression: string): FromCloudFormationFactoryVisitor { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once we get rid of the depth, this doesn't really need any instance variables anymore, and the "copy" word implies a lot of statekeeping that is making me uncomfortable. Wouldn't factory functions make more sense?
FromCloudFormationFactoryVisitor.fromExpression(...)
// ^--- this is a little long then, might want to rename it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can't get rid of depth
because of #9442 (comment)
Hey something just popped into my head -- if the thing the mappers return are themselves functions (which I think they are), then we don't need to render any lambdas, do we? Because:
Might simplify the code some more. Do we really really need the lambdas? |
We can't really render functions in the Let's take CloudFront's function CfnDistributionPropsFromCloudFormation(properties: any): CfnDistributionProps {
properties = properties || {};
return {
distributionConfig: DistributionConfigPropertyFromCloudFormation(properties.DistributionConfig),
// ...
};
} where However, if there is a different property that takes an optional function SomePropsFromCloudFormation(properties: any): SomeProps {
properties = properties || {};
return {
distributionConfig: properties.DistributionConfig == null ? undefined : DistributionConfigPropertyFromCloudFormation(properties.DistributionConfig),
// ...
};
} So |
We incorrectly handled union-types in the fromCloudFormation() generated code. Also we merged the 'Transform' sections of the CloudFormation template incorrectly, which has also been fixed in this change.
c74e4e3
to
037011d
Compare
Ah, but using higher order functions we could: function fromOptional(innerMapper: (x: any) => any) {
return (x: any) => x != null ? innerMapper(x) : undefined;
}
function SomePropsFromCloudFormation(properties: any): SomeProps {
properties = properties || {};
return {
distributionConfig: fromOptional(DistributionConfigPropertyFromCloudFormation)(properties.DistributionConfig),
// ...
};
} I'm not saying there has to be exactly one function that we return the name of. I'm saying we return an expression that evaluates to a function. In this case: |
tools/cfn2ts/lib/codegen.ts
Outdated
|
||
const arg = `prop${this.depth}`; | ||
// we need a deeper copy here, as 'mappers' is used in a lambda expression below | ||
const deeperVisitor = this.deeperCopy(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, this has the goal of JUST increasing depth
, which is not super clear to me.
Two big changes: * Make the parsing functions "lazy". They don't need to deserialize a value right then and there. Instead, they return a function that will do the promised deserialization. This removes the need for Lambdas and hence for unique variable names. * Optional-ness is an attribute of a "property", not of a "type". It therefore only needs to be considered at the top-level of the CfnFactoryFunction, and not in any of the deeper deserializers. It is therefore state that can be removed from `FromCloudFormationFactoryVisitor`. Similar for tags (I hope, because I'm not too sure what that code was trying to achieve). At the end of this, `FromCloudFormationFactoryVisitor` is now stateless and could be a singleton (but I left the class in place).
I added a commit showing what I mean, with a description of the change in the commit body. Having the visitor return a function which is then applied to the actual value is also how the serializer works, so it makes for a nice symmetry, see here: https://github.com/aws/aws-cdk/blob/master/tools/cfn2ts/lib/codegen.ts#L502 If you don't like it you can revert it (but I hope you wouldn't). Might have made some mistakes around tags, btw. Although I guess the only reason to have the 'as any' there was to escape the type system, and it seems to work equally well at the top level... |
Looks nice, thanks! I agree it's simpler. For tags, all of the tests pass, so I assume it's working 😃. |
Thank you for contributing! Your pull request will be updated from master and then merged automatically (do not update manually, and be sure to allow changes to be pushed to your fork). |
AWS CodeBuild CI Report
Powered by github-codebuild-logs, available on the AWS Serverless Application Repository |
Thank you for contributing! Your pull request will be updated from master and then merged automatically (do not update manually, and be sure to allow changes to be pushed to your fork). |
We incorrectly handled union-types in the fromCloudFormation() generated code. Also we merged the 'Transform' sections of the CloudFormation template incorrectly, which has also been fixed in this change. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
We incorrectly handled union-types in the fromCloudFormation() generated code.
Also we merged the 'Transform' sections of the CloudFormation template incorrectly,
which has also been fixed in this change.
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license