Skip to content

Commit

Permalink
Added schema validation for string literal attribute values
Browse files Browse the repository at this point in the history
  • Loading branch information
justinwilaby committed Mar 28, 2018
1 parent 76a418e commit a61307c
Show file tree
Hide file tree
Showing 10 changed files with 102 additions and 70 deletions.
2 changes: 1 addition & 1 deletion code.acx
Expand Up @@ -3,7 +3,7 @@ export class AdaptiveCardComponent {
return (
<card>
<body>
<text size="medium" weight="bolder">My First ACX card!</text>
<text size="medium" horizontalAlignment="center" weight="bolder">My First ACX card!</text>
</body>
<actions>
<action type="submit">
Expand Down
2 changes: 1 addition & 1 deletion example.js
Expand Up @@ -8,4 +8,4 @@ const output = babel.transform(code, {
presets: ['@babel/preset-env']
}).code;

console.log(output);
console.log(output);
1 change: 0 additions & 1 deletion examples/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions lib/helpers/defineElement.js
@@ -0,0 +1,5 @@
module.exports = function ($$kind) {
const element = {$$kind};
Object.defineProperty(element, 'attrs', {value: {}, enumerable: false});
return element;
};
11 changes: 8 additions & 3 deletions lib/helpers/getDefinitionFromSchema.js
@@ -1,7 +1,7 @@
const schema = require('../schema');
const definitionsMap = require('../utils/definitionsMap');

module.exports = function getDefinitionFromSchema(path, kind, type) {
function getDefinitionFromSchema(path, kind, type) {
let definition = type ? schema.definitions[definitionsMap[kind][type]] : schema.definitions[definitionsMap[kind]];
if (!definition) {
const message = type ? `${type} is not a valid type for the ${kind} element.` : `${kind} is not a valid adaptive card element.`;
Expand All @@ -11,15 +11,20 @@ module.exports = function getDefinitionFromSchema(path, kind, type) {
const {properties = {}} = definition;

definition.allOf.forEach(all => {
const additionalProps = getValueFrom(`${all.$ref}/properties`);
const additionalProps = getValueFromSchema(`${all.$ref}/properties`);
Object.assign(properties, additionalProps);
});
definition.properties = properties;
}
return definition;
};

function getValueFrom($ref) {
function getValueFromSchema($ref) {
const path = $ref.replace('#/', '').split('/');
return path.reduce((acc, fragment) => acc[fragment], schema);
}

module.exports = {
getDefinitionFromSchema,
getValueFromSchema
};
4 changes: 3 additions & 1 deletion lib/helpers/index.js
Expand Up @@ -2,6 +2,8 @@ module.exports.getExpressionKey = require('./getExpressionKey');
module.exports.getJSXElement = require('./getJSXElement');
module.exports.normalizeWhitespace = require('./normalizeWhitespace');
module.exports.getMapKeyForNode = require('./getMapKeyForNode');
module.exports.getDefinitionFromSchema = require('./getDefinitionFromSchema');
module.exports.getDefinitionFromSchema = require('./getDefinitionFromSchema').getDefinitionFromSchema;
module.exports.getValueFromSchema = require('./getDefinitionFromSchema').getValueFromSchema;
module.exports.normailizeWhitespace = require('./normalizeWhitespace');
module.exports.syntaxErrorThrower = require('./syntaxErrorThrower');
module.exports.defineElement = require('./defineElement');
2 changes: 2 additions & 0 deletions lib/index.js
Expand Up @@ -3,6 +3,7 @@ const {
JSXElement,
JSXExpressionContainer,
JSXFragment,
JSXAttribute,
JSXSpreadAttribute,
JSXSpreadChild,
Program
Expand All @@ -16,6 +17,7 @@ module.exports = function (api, options) {
JSXElement,
JSXFragment,
JSXSpreadChild,
JSXAttribute,
JSXSpreadAttribute,
JSXExpressionContainer
}
Expand Down
95 changes: 32 additions & 63 deletions lib/utils/builders.js
@@ -1,16 +1,14 @@
const t = require('@babel/types');

const {getMapKeyForNode, normalizeWhitespace, syntaxErrorThrower} = require('../helpers');
const {getMapKeyForNode, normalizeWhitespace, syntaxErrorThrower, defineElement} = require('../helpers');
const definitionsMap = require('./definitionsMap');
const getDefinitionFromSchema = require('../helpers/getDefinitionFromSchema');

module.exports.action = function (path, scope) {
const action = {$$kind: 'action'};
copyAttributes(path, action);
const action = defineElement('action');
const parent = getParent(path, scope);
const type = action.attrs.type;
const typeAttr = path.node.openingElement.attributes.find(attr => (!t.isJSXSpreadAttribute(attr) && attr.name.name === 'type'));
const children = path.node.children;
action.attrs.type = definitionsMap.action[type.value];
action.attrs.type = definitionsMap.action[typeAttr.value.value];
const childPropertyName = findActionChildProperty(action);

// Let the visitor handle the card build
Expand All @@ -35,7 +33,9 @@ module.exports.action = function (path, scope) {

module.exports.actions = function (path, scope) {
const parent = getParent(path, scope);
const actions = {$$kind: 'actions', actions: []};
const actions = defineElement('actions');
actions.actions = [];

if (path.node.openingElement.attributes.length) {
syntaxErrorThrower(path, 'The actions element cannot contain attributes');
}
Expand All @@ -62,9 +62,12 @@ module.exports.body = function (path, scope) {

module.exports.card = function (path, scope) {
const parent = getParent(path, scope);
const card = {$$kind: 'card', attrs: {version: '1.0', $schema: 'http://adaptivecards.io/schemas/adaptive-card.json'}};
copyAttributes(path, card);
card.attrs.type = definitionsMap.card;
const card = defineElement('card');
Object.assign(card.attrs, {
type: definitionsMap.card,
version: '1.0',
$schema: 'http://adaptivecards.io/schemas/adaptive-card.json'
});

if (parent && parent.attrs && parent.attrs.type === definitionsMap.action.showCard) {
parent.card = card;
Expand All @@ -76,9 +79,8 @@ module.exports.card = function (path, scope) {
};

module.exports.choice = function (path, scope) {
const choice = {$$kind: 'choice'};
const choice = defineElement('choice');
const parent = getParent(path, scope);
copyAttributes(path, choice);
choice.title = path.node.children.filter((childNode) => t.isJSXExpressionContainer(childNode) || normalizeWhitespace(childNode.value) !== '');
choice.attrs.type = definitionsMap.choice;
if (parent && parent.attrs && parent.attrs.type === definitionsMap.input.choiceSet) {
Expand All @@ -90,8 +92,8 @@ module.exports.choice = function (path, scope) {

module.exports.column = function (path, scope) {
const parent = getParent(path, scope);
const column = {$$kind: 'column', items: []};
copyAttributes(path, column);
const column = defineElement('column');
column.items = [];
column.attrs.type = definitionsMap.column;

if (parent && parent.attrs && parent.attrs.type === definitionsMap.columns) {
Expand All @@ -105,8 +107,8 @@ module.exports.column = function (path, scope) {

module.exports.columns = function (path, scope) {
const parent = getParent(path, scope);
const columns = {$$kind: 'columns', columns: []};
copyAttributes(path, columns);
const columns = defineElement('columns');
columns.columns = [];
columns.attrs.type = definitionsMap.columns;

if (parent && parent.attrs && parent.attrs.type === definitionsMap.container) {
Expand All @@ -126,11 +128,6 @@ module.exports.component = function (path, scope) {
props: {},
children: []
};
(path.node.openingElement.attributes || []).forEach(attr => {
if (!t.isJSXSpreadAttribute(attr)) {
component.props[attr.name.name] = attr.value;
}
});
if (!insertElementViaCommonParentProps(component, parent)) {
switch (parent.$$kind) {
case 'columns':
Expand Down Expand Up @@ -162,17 +159,16 @@ module.exports.component = function (path, scope) {

module.exports.container = function (path, scope) {
const parent = getParent(path, scope);
const container = {$$kind: 'container', items: []};
copyAttributes(path, container);
const container = defineElement('container');
container.items = [];
container.attrs.type = definitionsMap.container;
insertElementViaCommonParentProps(container, parent);
return container;
};

module.exports.fact = function (path, scope) {
const parent = getParent(path, scope);
const fact = {$$kind: 'fact'};
copyAttributes(path, fact);
const fact = defineElement('fact');
fact.attrs.type = definitionsMap.fact;

if (!insertElementViaCommonParentProps(fact, parent)) {
Expand All @@ -183,8 +179,8 @@ module.exports.fact = function (path, scope) {

module.exports.facts = function (path, scope) {
const parent = getParent(path, scope);
const facts = {$$kind: 'facts', facts: []};
copyAttributes(path, facts);
const facts = defineElement('facts');
facts.facts = [];
facts.attrs.type = definitionsMap.facts;

if (!parent) {
Expand All @@ -200,13 +196,13 @@ module.exports.facts = function (path, scope) {

module.exports.image = function (path, scope) {
const parent = getParent(path, scope);
const image = {$$kind: 'image'};
const image = defineElement('image');

copyAttributes(path, image);
image.attrs.type = definitionsMap.image;
if (!parent) {
return image;
}
image.attrs.type = definitionsMap.image;

if (parent.attrs && parent.attrs.type === definitionsMap.images) {
parent.images.push(image);
} else if (/(column|container)/.test(parent.$$kind)) {
Expand All @@ -220,20 +216,19 @@ module.exports.image = function (path, scope) {

module.exports.images = function (path, scope) {
const parent = getParent(path, scope);
const images = {$$kind: 'images', images: []};
copyAttributes(path, images);
const images = defineElement('images');
images.images = [];
images.attrs.type = definitionsMap.images;
insertElementViaCommonParentProps(images, parent);

return images;
};

module.exports.input = function (path, scope) {
const input = {$$kind: 'input'};
const input = defineElement('input');
const parent = getParent(path, scope);
copyAttributes(path, input, scope.opts.expressions);

input.attrs.type = definitionsMap.input[input.attrs.type.value];
const typeAttr = path.node.openingElement.attributes.find(attr => (!t.isJSXSpreadAttribute(attr) && attr.name.name === 'type'));
input.attrs.type = definitionsMap.input[typeAttr.value.value];
const {children} = path.node;
if (children && children.length) {
switch (input.attrs.type) {
Expand All @@ -258,9 +253,8 @@ module.exports.input = function (path, scope) {
};

module.exports.text = function (path, scope) {
const text = {$$kind: 'text'};
const text = defineElement('text');
const parent = getParent(path, scope);
copyAttributes(path, text);
text.text = path.node.children.filter(childNode => t.isJSXExpressionContainer(childNode) || normalizeWhitespace(childNode.value) !== '');
text.attrs.type = definitionsMap.text;
if (parent && /(column|container)/.test(parent.$$kind)) {
Expand All @@ -271,31 +265,6 @@ module.exports.text = function (path, scope) {
return text;
};

function copyAttributes(path, destination) {
if (!destination.attrs) {
Object.defineProperty(destination, 'attrs', {value: {}, enumerable: false});
}
const typeAttr = path.node.openingElement.attributes.find(attr => (!t.isJSXSpreadAttribute(attr) && attr.name.name === 'type'));
if (typeAttr && !t.isStringLiteral(typeAttr.value)) {
syntaxErrorThrower(path, 'Type attributes must be string literals');
}
const type = typeAttr ? typeAttr.value.value : null;
const definition = getDefinitionFromSchema(path, destination.$$kind, type);
if (!definition.properties && path.node.openingElement.attributes.length) {
syntaxErrorThrower(path, `${destination.$$kind} does not allow attributes`);
}

(path.node.openingElement.attributes || []).forEach(attr => {
if (!t.isJSXSpreadAttribute(attr)) {
const name = attr.name.name;
if (!definition.properties[name] || (destination[name] && destination.attrs[name])) {
syntaxErrorThrower(path, `The ${name} attribute is not allowed here`);
}
destination.attrs[attr.name.name] = attr.value;
}
});
}

function getParent(path, scope) {
const key = getMapKeyForNode(path.parent);
return scope.opts.nodeMap[key];
Expand Down
1 change: 1 addition & 0 deletions lib/visitors/index.js
@@ -1,6 +1,7 @@
module.exports.JSXFragment = require('./jsxFragment');
module.exports.JSXElement = require('./jsxElement');
module.exports.JSXSpreadChild = require('./jsxSpreadChild');
module.exports.JSXAttribute = require('./jsxAttribute');
module.exports.JSXSpreadAttribute = require('./jsxSpreadAttribute');
module.exports.JSXExpressionContainer = require('./jsxExpressionContainer');
module.exports.Program = require('./program');
49 changes: 49 additions & 0 deletions lib/visitors/jsxAttribute.js
@@ -0,0 +1,49 @@
const t = require('@babel/types');
const {getMapKeyForNode, getJSXElement, syntaxErrorThrower, getDefinitionFromSchema, getValueFromSchema} = require('../helpers');

module.exports = {
enter(path, scope) {
const jsxElement = getJSXElement(t, path);
const key = getMapKeyForNode(jsxElement);
const element = scope.opts.nodeMap[key];
const attr = path.node;
const name = attr.name.name;
const isSpreadAttr = t.isJSXSpreadAttribute(attr);
if (element.$$kind === 'component' && !isSpreadAttr) {
element.props[name] = attr.value;
return;
}

const typeAttr = jsxElement.openingElement.attributes.find(attr => (!t.isJSXSpreadAttribute(attr) && attr.name.name === 'type'));
if (typeAttr && !t.isStringLiteral(typeAttr.value)) {
syntaxErrorThrower(path, 'Type attributes must be string literals');
}

const type = typeAttr ? typeAttr.value.value : null;
const definition = getDefinitionFromSchema(path, element.$$kind, type);
if (!definition.properties && path.node.openingElement.attributes.length) {
syntaxErrorThrower(path, `${element.$$kind} does not allow attributes`);
}

if (!isSpreadAttr && !(name in element.attrs)) {

if (!definition.properties[name] || (element[name] && element.attrs[name])) {
syntaxErrorThrower(path, `The ${name} attribute is not allowed here`);
}

// Validation can only occur when we have a string literal
if (t.isStringLiteral(attr.value)) {
const {value} = attr.value;
const attributeDefinition = definition.properties[name];
let {enum:enums, $ref} = attributeDefinition;
if ($ref) {
enums = getValueFromSchema($ref).enum;
}
if (enums && !enums.includes(value)) {
syntaxErrorThrower(path, `${value} is not a valid value for the ${name} attribute`);
}
}
element.attrs[name] = attr.value;
}
}
};

0 comments on commit a61307c

Please sign in to comment.