ask-cli-x
Version:
Alexa Skills Kit (ASK) Command Line Interfaces
206 lines (205 loc) • 9.81 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.tryMapField = exports.tryMapReference = exports.mapExpressions = exports.simpleExpressionMap = exports.tryMapTypes = exports.encodeTypeName = void 0;
const acdl_1 = require("@alexa/acdl");
const cli_error_1 = __importDefault(require("../../../exceptions/cli-error"));
const encodeTypeName = (type) => {
var _a, _b, _c, _d;
if (!type) {
return;
}
if ((((_a = type.genericArguments) === null || _a === void 0 ? void 0 : _a.length) || 0) > 0) {
return `${(_b = type.name) === null || _b === void 0 ? void 0 : _b.name}||${(_c = type.genericArguments) === null || _c === void 0 ? void 0 : _c.map((arg) => (0, exports.encodeTypeName)(arg)).join("||")}`;
}
return `${(_d = type.name) === null || _d === void 0 ? void 0 : _d.name}`;
};
exports.encodeTypeName = encodeTypeName;
/**
* Lossy attempt to map two possibly related types to each other.
*
* Note: This implementation is hacky. Try to improve the mapping algorithm as needed.
*
* Rules:
* 1. All property mappings much be pointing to the same type in both structures.
* 2. First look for exact property name matches (within the type).
* 3. Then match positionally, with the first instance of a property of a type in the target being matched to the first instance in the source.
* Note: This is after exact matches are removed.
* 4. If there are more instances of a property of a type in the source than the target, map the property name to the first
* property of that type in the target, no matter if its been used before.
* @returns a mapping from the fields in source to the opinionated match in target.
* if the source or both types have no properties, an empty collection is returned.
* @throws If the target has no properties and the source does have properties.
* If the source contains a property of a type that is not present in the source.
*/
const tryMapTypes = (source, target) => {
var _a, _b, _c, _d;
if (!target.properties) {
// Neither have properties! Return empty.
if (!source.properties) {
return {};
}
throw new cli_error_1.default(`Target type (${(_a = target.name) === null || _a === void 0 ? void 0 : _a.name}) has no properties. There are no properties to map from the source type (${(_b = source.name) === null || _b === void 0 ? void 0 : _b.name}).`);
}
if (!source.properties) {
// If the source has no properties, it doesn't matter what the target is.
return {};
}
const targetTypes = target.properties
.filter((p) => !!p.type)
.map((p) => [(0, exports.encodeTypeName)(p.type), p.name])
.filter((typeAndName) => !!typeAndName[0] && !!typeAndName[1])
.reduce((acc, [type, name]) => ({
...acc,
[type]: type in acc ? [...acc[type], name] : [name],
}), {});
const sourceTypes = source.properties
.map((p) => [(0, exports.encodeTypeName)(p.type), p.name])
.filter((typeAndName) => !!typeAndName[0] && !!typeAndName[1])
.reduce((acc, [type, name]) => ({
...acc,
[type]: type in acc ? [...acc[type], name] : [name],
}), {});
if (!Object.keys(sourceTypes).every((t) => Object.keys(targetTypes).includes(t))) {
throw new cli_error_1.default(`Source type (${(_c = source.name) === null || _c === void 0 ? void 0 : _c.name}) references a property of a type missing in the target type (${(_d = target.name) === null || _d === void 0 ? void 0 : _d.name}).`);
}
return Object.entries(sourceTypes).reduce((acc, [type, names]) => {
const targetNames = targetTypes[type];
const matchingNames = names.filter((name) => targetNames.includes(name));
// Remove the exact map names from both collections.
const remainingNames = names.filter((name) => !matchingNames.includes(name));
const remainingTargetNames = targetNames.filter((name) => !matchingNames.includes(name));
// Add exact matches to the map...
const mappings = matchingNames.reduce((_acc, name) => ({ ..._acc, [name]: { name, encodedTypeName: type } }), acc);
const [mapping] = remainingNames.reduce(([_acc, rem], name) => {
const [first, ...rest] = rem;
// if the next position
if (first) {
return [
{
..._acc,
[name]: { name: first, encodedTypeName: type },
},
rest,
];
}
// The target has fewer instances of properties of this type than the source...
// Just take the first name?
else {
return [
{
..._acc,
[name]: { name: targetNames[0], encodedTypeName: type },
},
[],
];
}
}, [mappings, remainingTargetNames]);
return mapping;
}, {});
};
exports.tryMapTypes = tryMapTypes;
/**
* Maps a set of expressions to itself.
*
* Allows for symmetrical resolution of ACIR to ASK.
*/
const simpleExpressionMap = (source, sourceProject) => (0, exports.mapExpressions)(source, source, sourceProject, sourceProject);
exports.simpleExpressionMap = simpleExpressionMap;
/**
* Name declaration mapping between two similar sequence of expressions.
*
* In MFT we receive a response from the server that contains a similar sequence of actions that may not
* look like the original expressions or expressions provided by the user.
*
* This method helps provide a singular, heuristic based, mapping between the two expression sequences.
*
* TODO: Current implementation assumes that there are the same number of named expressions in each sequence.
* Next iteration may try to match on the type of the name and optimize for the best match.
*
* @throws if the target and source have a different number of named expressions.
*/
const mapExpressions = (sources, targets, sourceProject, targetProject) => {
if (sources.length > targets.length) {
throw new cli_error_1.default("Source cannot have more expressions declarations than the target.");
}
// TODO: Improve this function by better mapping pairs.
const sourceTargetPairs = sources.map((source, i) => ({ source, target: targets[i] }));
return (sourceTargetPairs
// only map when both source and target are named.
.filter((x) => { var _a, _b; return (0, acdl_1.isNameDeclaration)(x.source) && !!((_a = x.source.name) === null || _a === void 0 ? void 0 : _a.name) && (0, acdl_1.isNameDeclaration)(x.target) && !!((_b = x.target.name) === null || _b === void 0 ? void 0 : _b.name); })
.map(({ source, target }, i) => {
var _a, _b;
const sourceType = extractType(source, sourceProject);
const targetType = extractType(target, targetProject);
if (!sourceType || !targetType) {
throw new cli_error_1.default(`Source ${(_a = source.name) === null || _a === void 0 ? void 0 : _a.name} or Target ${(_b = target.name) === null || _b === void 0 ? void 0 : _b.name} Type could not be found.`);
}
return {
/**
* Used to track the general order of mapped type to break ties.
*/
order: i,
// Generally frown on not null assertions, but we are checking it when filtering, shouldn't be null anyways.
targetReference: target.name.name,
sourceReference: source.name.name,
typeMapping: (0, exports.tryMapTypes)(sourceType, targetType),
};
})
.reduce((acc, { sourceReference, ...rest }) => ({
...acc,
[sourceReference]: rest,
}), {}));
};
exports.mapExpressions = mapExpressions;
/**
* Given an expression map, try to find a name that best matches the given fieldName.
*
* Optionally maps a single segment field name to a field in the selected expression.
* If the field is NOT found on the selected reference, field is returned as undefined.
*
* TODO: Improve heuristics like type matching.
*/
const tryMapReference = (name, expressionMap, fieldName) => {
// candidates have a matching field.
if (name in expressionMap) {
const exp = expressionMap[name];
if (fieldName) {
if (fieldName in exp.typeMapping) {
return { reference: exp.targetReference, field: exp.typeMapping[fieldName].name };
}
}
return { reference: exp.targetReference };
}
return;
};
exports.tryMapReference = tryMapReference;
/**
* Given an expression map, try to find a property name that best matches the given fieldName.
*
* Supports only a single segment field name.
*
* TODO: Improve heuristics like type matching.
*/
const tryMapField = (fieldName, expressionMap) => {
// candidates have a matching field.
const candidates = Object.values(expressionMap)
.filter(({ typeMapping }) => fieldName in typeMapping)
.map((value) => ({
reference: value.targetReference,
field: value.typeMapping[fieldName].name,
order: value.order,
}));
// uhh oh, no candidates
if (candidates.length == 0)
return;
// Take the lowest order instance of a field in the map, based on the order.
return candidates.sort((a, b) => (a.order = b.order))[0];
};
exports.tryMapField = tryMapField;
const extractType = (named, project) => {
const checker = project.getTypeChecker();
return checker.getType(named);
};