-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(sam): CfnFunction events are not rendered (#26679)
Because of a mistake introduced into the SAM schema, the `AlexaSkill` event type doesn't have any required properties anymore. When the `CfnFunction` is trying all the different event types in the type union that it supports, it will go through every type in alphabetical order and pick the first type that doesn't fail its validation. After the schema change, the first type (`Alexa` which starts with an `A`) would therefore accept all types: no required fields, and for JavaScript compatibility purposes we allow superfluous fields, and so we pick a type that doesn't render anything. This change reorders the alternatives in the union such that stronger types are tried first. `HttpApiEvent` and `AlexaSkillEvent` both have no required properties, and this now reverses the problem: `AlexaSkillEvent` can no longer be specified because `HttpApiEvent` will match first. But that's the more common use case, so better for now, while we wait for the spec fix to come in, to prefer the HTTP API. Relates to #26637. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
- Loading branch information
Showing
9 changed files
with
264 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import * as assertions from '../../../assertions'; | ||
import * as cdk from '../../../core'; | ||
import * as sam from '../../lib'; | ||
|
||
test('generation of alts from CfnFunction', () => { | ||
const app = new cdk.App(); | ||
const stack = new cdk.Stack(app, 'Stack'); | ||
new sam.CfnFunction(stack, 'MyAPI', { | ||
codeUri: 'build/', | ||
events: { | ||
GetResource: { | ||
type: 'Api', | ||
properties: { | ||
restApiId: '12345', | ||
path: '/myPath', | ||
method: 'POST', | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
const template = assertions.Template.fromStack(stack); | ||
template.hasResourceProperties('AWS::Serverless::Function', { | ||
Events: { | ||
GetResource: { | ||
Properties: { | ||
Method: 'POST', | ||
Path: '/myPath', | ||
RestApiId: '12345', | ||
}, | ||
}, | ||
}, | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import { PropertyType, RichPropertyType, SpecDatabase, TypeDefinition } from '@aws-cdk/service-spec-types'; | ||
import { Type } from '@cdklabs/typewriter'; | ||
import { isSubsetOf } from '../util/sets'; | ||
import { topologicalSort } from '../util/toposort'; | ||
|
||
/** | ||
* Order types for use in a union | ||
* Order the types such that the types with the most strength (i.e., excluding the most values from the type) are checked first | ||
* | ||
* This is necessary because at runtime, the union checker will iterate | ||
* through the types one-by-one to check whether a value inhabits a type, and | ||
* it will stop at the first one that matches. | ||
* | ||
* We therefore shouldn't have the weakest type up front, because we'd pick the wrong type. | ||
*/ | ||
export class UnionOrdering { | ||
constructor(private readonly db: SpecDatabase) {} | ||
|
||
/** | ||
* Order typewriter Types based on the strength of the associated PropertyTypes | ||
*/ | ||
public orderTypewriterTypes(writerTypes: Type[], propTypes: PropertyType[]): Type[] { | ||
if (writerTypes.length !== propTypes.length) { | ||
throw new Error('Arrays need to be the same length'); | ||
} | ||
|
||
const correspondence = new Map<PropertyType, Type>(); | ||
for (let i = 0; i < writerTypes.length; i++) { | ||
correspondence.set(propTypes[i], writerTypes[i]); | ||
} | ||
|
||
return this.orderPropertyTypes(propTypes).map((t) => assertTruthy(correspondence.get(t))); | ||
} | ||
|
||
/** | ||
* Order PropertyTypes, strongest first | ||
*/ | ||
public orderPropertyTypes(types: PropertyType[]): PropertyType[] { | ||
// Map { X -> [Y] }, indicating that X is weaker than each of Y | ||
const afterMap = new Map<PropertyType, PropertyType[]>(types.map((type) => [ | ||
type, | ||
types.filter((other) => !new RichPropertyType(type).equals(other) && this.strongerThan(other, type)), | ||
])); | ||
return topologicalSort(types, (t) => t, (t) => afterMap.get(t) ?? []); | ||
} | ||
|
||
/** | ||
* Whether type A is strictly stronger than type B (and hence should be tried before B) | ||
* | ||
* Currently only specialized if both types are type declarations, otherwise we default | ||
* to the general kind of the type. | ||
*/ | ||
private strongerThan(a: PropertyType, b: PropertyType) { | ||
const aType = a.type === 'ref' ? this.db.get('typeDefinition', a.reference.$ref) : undefined; | ||
const bType = b.type === 'ref' ? this.db.get('typeDefinition', b.reference.$ref) : undefined; | ||
if (aType && bType) { | ||
const aReq = requiredPropertyNames(aType); | ||
const bReq = requiredPropertyNames(bType); | ||
|
||
// If the required properties of A are a proper supserset of B, A goes first (== B is a proper subset of A) | ||
const [aSubB, bSubA] = [isSubsetOf(aReq, bReq), isSubsetOf(bReq, aReq)]; | ||
if (aSubB !== bSubA) { | ||
return bSubA; | ||
} | ||
|
||
// Otherwise, the one with more required properties goes first | ||
if (aReq.size !== bReq.size) { | ||
return aReq.size > bReq.size; | ||
} | ||
|
||
// Otherwise the one with the most total properties goes first | ||
return Object.keys(aType.properties).length > Object.keys(bType.properties).length; | ||
} | ||
return basicKindStrength(a) < basicKindStrength(b); | ||
|
||
/** | ||
* Return an order for the kind of the type, lower is stronger. | ||
*/ | ||
function basicKindStrength(x: PropertyType): number { | ||
switch (x.type) { | ||
case 'array': | ||
return 0; | ||
case 'date-time': | ||
return 1; | ||
case 'string': | ||
return 2; | ||
case 'number': | ||
case 'integer': | ||
return 3; | ||
case 'null': | ||
return 4; | ||
case 'boolean': | ||
return 5; | ||
case 'ref': | ||
return 6; | ||
case 'tag': | ||
return 7; | ||
case 'map': | ||
// Must be higher than type declaration, because they will look the same in JS | ||
return 8; | ||
case 'union': | ||
return 9; | ||
case 'json': | ||
// Must have the highest number of all | ||
return 100; | ||
} | ||
} | ||
} | ||
} | ||
|
||
function requiredPropertyNames(t: TypeDefinition): Set<string> { | ||
return new Set(Object.entries(t.properties).filter(([_, p]) => p.required).map(([n, _]) => n)); | ||
} | ||
|
||
function assertTruthy<T>(x: T): NonNullable<T> { | ||
if (x == null) { | ||
throw new Error('Expected truhty value'); | ||
} | ||
return x; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
/** | ||
* Whether A is a subset of B | ||
*/ | ||
export function isSubsetOf<T>(as: Set<T>, bs: Set<T>) { | ||
for (const a of as) { | ||
if (!bs.has(a)) { | ||
return false; | ||
} | ||
} | ||
return true; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
export type KeyFunc<T, K> = (x: T) => K; | ||
export type DepFunc<T, K> = (x: T) => K[]; | ||
|
||
/** | ||
* Return a topological sort of all elements of xs, according to the given dependency functions | ||
* | ||
* Dependencies outside the referenced set are ignored. | ||
* | ||
* Not a stable sort, but in order to keep the order as stable as possible, we'll sort by key | ||
* among elements of equal precedence. | ||
*/ | ||
export function topologicalSort<T, K=string>(xs: Iterable<T>, keyFn: KeyFunc<T, K>, depFn: DepFunc<T, K>): T[] { | ||
const remaining = new Map<K, TopoElement<T, K>>(); | ||
for (const element of xs) { | ||
const key = keyFn(element); | ||
remaining.set(key, { key, element, dependencies: depFn(element) }); | ||
} | ||
|
||
const ret = new Array<T>(); | ||
while (remaining.size > 0) { | ||
// All elements with no more deps in the set can be ordered | ||
const selectable = Array.from(remaining.values()).filter(e => e.dependencies.every(d => !remaining.has(d))); | ||
|
||
selectable.sort((a, b) => a.key < b.key ? -1 : b.key < a.key ? 1 : 0); | ||
|
||
for (const selected of selectable) { | ||
ret.push(selected.element); | ||
remaining.delete(selected.key); | ||
} | ||
|
||
// If we didn't make any progress, we got stuck | ||
if (selectable.length === 0) { | ||
throw new Error(`Could not determine ordering between: ${Array.from(remaining.keys()).join(', ')}`); | ||
} | ||
} | ||
|
||
return ret; | ||
} | ||
|
||
interface TopoElement<T, K> { | ||
key: K; | ||
dependencies: K[]; | ||
element: T; | ||
} |