diff --git a/packages/babel-types/scripts/utils/stringifyValidator.js b/packages/babel-types/scripts/utils/stringifyValidator.js index ff33e8e25ade..2ea1e80357dd 100644 --- a/packages/babel-types/scripts/utils/stringifyValidator.js +++ b/packages/babel-types/scripts/utils/stringifyValidator.js @@ -31,6 +31,29 @@ module.exports = function stringifyValidator(validator, nodePrefix) { return validator.type; } + if (validator.shapeOf) { + return ( + "{ " + + Object.keys(validator.shapeOf) + .map(shapeKey => { + const propertyDefinition = validator.shapeOf[shapeKey]; + if (propertyDefinition.validate) { + const isOptional = + propertyDefinition.optional || propertyDefinition.default != null; + return ( + shapeKey + + (isOptional ? "?: " : ": ") + + stringifyValidator(propertyDefinition.validate) + ); + } + return null; + }) + .filter(Boolean) + .join(", ") + + " }" + ); + } + return ["any"]; }; diff --git a/packages/babel-types/src/definitions/es2015.js b/packages/babel-types/src/definitions/es2015.js index fb42b1390583..88da2cdd1181 100644 --- a/packages/babel-types/src/definitions/es2015.js +++ b/packages/babel-types/src/definitions/es2015.js @@ -1,5 +1,6 @@ // @flow import defineType, { + assertShape, assertNodeType, assertValueType, chain, @@ -537,7 +538,15 @@ defineType("TemplateElement", { builder: ["value", "tail"], fields: { value: { - // todo: flatten `raw` into main node + validate: assertShape({ + raw: { + validate: assertValueType("string"), + }, + cooked: { + validate: assertValueType("string"), + optional: true, + }, + }), }, tail: { validate: assertValueType("boolean"), diff --git a/packages/babel-types/src/definitions/utils.js b/packages/babel-types/src/definitions/utils.js index c1d2ce762daa..3f7225bd50b2 100644 --- a/packages/babel-types/src/definitions/utils.js +++ b/packages/babel-types/src/definitions/utils.js @@ -1,5 +1,6 @@ // @flow import is from "../validators/is"; +import { validateField } from "../validators/validate"; export const VISITOR_KEYS: { [string]: Array } = {}; export const ALIAS_KEYS: { [string]: Array } = {}; @@ -161,6 +162,34 @@ export function assertValueType(type: string): Validator { return validate; } +export function assertShape(shape: { [string]: FieldOptions }): Validator { + function validate(node, key, val) { + const errors = []; + for (const property of Object.keys(shape)) { + try { + validateField(node, property, val[property], shape[property]); + } catch (error) { + if (error instanceof TypeError) { + errors.push(error.message); + continue; + } + throw error; + } + } + if (errors.length) { + throw new TypeError( + `Property ${key} of ${ + node.type + } expected to have the following:\n${errors.join("\n")}`, + ); + } + } + + validate.shapeOf = shape; + + return validate; +} + export function chain(...fns: Array): Validator { function validate(...args) { for (const fn of fns) { diff --git a/packages/babel-types/src/validators/validate.js b/packages/babel-types/src/validators/validate.js index 9b2e9a8cd02a..738683849927 100644 --- a/packages/babel-types/src/validators/validate.js +++ b/packages/babel-types/src/validators/validate.js @@ -8,6 +8,15 @@ export default function validate(node?: Object, key: string, val: any): void { if (!fields) return; const field = fields[key]; + validateField(node, key, val, field); +} + +export function validateField( + node?: Object, + key: string, + val: any, + field: any, +): void { if (!field || !field.validate) return; if (field.optional && val == null) return; diff --git a/packages/babel-types/test/builders/es2015/__snapshots__/templateElement.js.snap b/packages/babel-types/test/builders/es2015/__snapshots__/templateElement.js.snap new file mode 100644 index 000000000000..a2f178169233 --- /dev/null +++ b/packages/babel-types/test/builders/es2015/__snapshots__/templateElement.js.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`builders es2015 templateElement should validate 1`] = ` +Object { + "tail": false, + "type": "TemplateElement", + "value": Object { + "cooked": "foo", + "raw": "foo", + }, +} +`; + +exports[`builders es2015 templateElement should validate 2`] = ` +Object { + "tail": false, + "type": "TemplateElement", + "value": Object { + "raw": "foo", + }, +} +`; + +exports[`builders es2015 templateElement should validate 3`] = ` +"Property value of TemplateElement expected to have the following: +Property raw expected type of string but got number" +`; + +exports[`builders es2015 templateElement should validate 4`] = ` +"Property value of TemplateElement expected to have the following: +Property cooked expected type of string but got number" +`; + +exports[`builders es2015 templateElement should validate 5`] = ` +"Property value of TemplateElement expected to have the following: +Property raw expected type of string but got undefined" +`; diff --git a/packages/babel-types/test/builders/es2015/templateElement.js b/packages/babel-types/test/builders/es2015/templateElement.js new file mode 100644 index 000000000000..0a93e7c8662a --- /dev/null +++ b/packages/babel-types/test/builders/es2015/templateElement.js @@ -0,0 +1,25 @@ +import * as t from "../../.."; + +describe("builders", function() { + describe("es2015", function() { + describe("templateElement", function() { + it("should validate", function() { + expect( + t.templateElement({ raw: "foo", cooked: "foo" }), + ).toMatchSnapshot(); + + expect(t.templateElement({ raw: "foo" })).toMatchSnapshot(); + + expect(() => + t.templateElement({ raw: 1 }), + ).toThrowErrorMatchingSnapshot(); + + expect(() => + t.templateElement({ raw: "foo", cooked: 1 }), + ).toThrowErrorMatchingSnapshot(); + + expect(() => t.templateElement("foo")).toThrowErrorMatchingSnapshot(); + }); + }); + }); +});