├── .babelrc ├── .gitignore ├── README.md ├── flow-runtime ├── gen.js ├── index.js ├── package.json └── validation.js ├── index.js ├── package.json └── use-cases ├── generative-testing.js ├── index.js ├── types.js └── validation.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["babel-preset-es2015"], 3 | "plugins": ["syntax-flow", "./index"], 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Runtime Flow Introspection 2 | ========================== 3 | 4 | https://medium.com/@joe_stant/runtime-introspection-of-flow-types-ddb7e5b042a5#.304i7i3rq 5 | 6 | # Babel Transform 7 | 8 | Exposes flow type annotations as a JS object that can be interpreted at runtime and powers the below features. 9 | 10 | # Features 11 | 12 | * [Type Conformance Validation](https://github.com/JoeStanton/babel-transform-flow-introspection/blob/master/use-cases/validation.js) 13 | * [Generating test cases for property-based/unit tests](https://github.com/JoeStanton/babel-transform-flow-introspection/blob/master/use-cases/generative-testing.js) 14 | -------------------------------------------------------------------------------- /flow-runtime/gen.js: -------------------------------------------------------------------------------- 1 | import range from "lodash.range"; 2 | 3 | export const setup = (Exports) => { 4 | const genObjectProp = function(def) { 5 | console.log(def.key.name); 6 | }; 7 | 8 | const genObject = function(def) { 9 | return (def.properties || []).reduce((acc, prop) => { 10 | acc[prop.key.name] = gen(prop.value); 11 | return acc; 12 | }, {}); 13 | }; 14 | 15 | const genString = () => "String"; 16 | const genNum = () => 1; 17 | const genUnion = (def) => def.types.map(x => generatorFor(x.type)(x))[0]; // Take the 0th for now 18 | const genNull = () => null; 19 | const genBool = () => true; 20 | const genVoid = () => void 8; 21 | const genLiteral = (def) => def.value; 22 | const genArrayOf = (def) => { 23 | const typeParams = def.typeParameters && def.typeParameters.params && def.typeParameters.params.map(gen) || []; 24 | return typeParams; 25 | }; 26 | const genGeneric = (def) => { 27 | const exportedName = def.id.name + "Type"; 28 | 29 | if (Exports[exportedName]) { 30 | return gen(Exports[exportedName]); 31 | } 32 | 33 | if (generators[def.id.name]) { 34 | return generators[def.id.name](def); 35 | } 36 | 37 | throw new Error("No type declaration found for " + def.id.name); 38 | }; 39 | const genFunc = (def) => () => gen(def.returnType); 40 | 41 | const generators = { 42 | "Object": genObject, 43 | "ObjectTypeAnnotation": genObject, 44 | "GenericTypeAnnotation": genGeneric, 45 | "AnyTypeAnnotation": genString, 46 | "MixedTypeAnnotation": genString, 47 | "StringTypeAnnotation": genString, 48 | "StringLiteralTypeAnnotation": genLiteral, 49 | "NumericLiteralTypeAnnotation": genLiteral, 50 | "NullLiteralTypeAnnotation": genLiteral, 51 | "NumberTypeAnnotation": genNum, 52 | "BooleanTypeAnnotation": genBool, 53 | "UnionTypeAnnotation": genUnion, 54 | "VoidTypeAnnotation": genVoid, 55 | "Array": genArrayOf, 56 | "Function": genFunc, 57 | "FunctionTypeAnnotation": genFunc, 58 | } 59 | 60 | const generatorFor = (type) => { 61 | if (!generators[type]) { 62 | throw new Error("No type generator found for " + type); 63 | } 64 | return generators[type]; 65 | } 66 | 67 | const gen = (type) => { 68 | if (!type) { 69 | throw new Error("Invalid type " + type); 70 | } 71 | return generatorFor(type.type)(type); 72 | } 73 | 74 | const genN = (type, n) => range(0, n).reduce((examples, _) => examples.concat([gen(type)]), []); 75 | 76 | return { gen, genN }; 77 | } 78 | -------------------------------------------------------------------------------- /flow-runtime/index.js: -------------------------------------------------------------------------------- 1 | // Entry point 2 | -------------------------------------------------------------------------------- /flow-runtime/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flow-runtime", 3 | "version": "0.0.1", 4 | "description": "Validation & Fixture Generation from Flow AST output", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Joe Stanton", 10 | "license": "ISC" 11 | } 12 | -------------------------------------------------------------------------------- /flow-runtime/validation.js: -------------------------------------------------------------------------------- 1 | import joi from "joi"; 2 | 3 | export const setup = (Exports) => { 4 | const generateSchema = (type) => { 5 | return validators[type.type](type); 6 | } 7 | 8 | const validateString = () => joi.string(); 9 | const validateNum = () => joi.number(); 10 | const validateBool = () => joi.boolean(); 11 | const validateVoid = () => joi.valid(void 8); 12 | const validateLiteral = (def) => joi.valid(def.value); 13 | const validateUnion = (def) => joi.only(def.types.map(x => x.value)); 14 | 15 | const validateObject = (def) => { 16 | return joi.object(def.properties.reduce((acc, prop) => { 17 | acc[prop.key.name] = generateSchema(prop.value); 18 | return acc; 19 | }, {}) 20 | ); 21 | } 22 | 23 | const validateArrayOf = (def) => { 24 | const typeParam = def.typeParameters && def.typeParameters.params && def.typeParameters.params[0]; 25 | 26 | if (!typeParam) { 27 | return joi.array(); 28 | } 29 | 30 | return joi.array().items(generateSchema(typeParam)); 31 | }; 32 | 33 | const validateGeneric = (def) => { 34 | const exportedName = def.id.name + "Type"; 35 | 36 | if (Exports[exportedName]) { 37 | return generateSchema(Exports[exportedName]); 38 | } 39 | 40 | if (validators[def.id.name]) { 41 | return validators[def.id.name](def); 42 | } 43 | 44 | throw new Error("No type declaration found for " + def.id.name); 45 | }; 46 | 47 | const validators = { 48 | "ObjectTypeAnnotation": validateObject, 49 | "GenericTypeAnnotation": validateGeneric, 50 | "AnyTypeAnnotation": validateString, 51 | "MixedTypeAnnotation": validateString, 52 | "StringTypeAnnotation": validateString, 53 | "StringLiteralTypeAnnotation": validateLiteral, 54 | "NumericLiteralTypeAnnotation": validateLiteral, 55 | "NullLiteralTypeAnnotation": validateLiteral, 56 | "NumberTypeAnnotation": validateNum, 57 | "BooleanTypeAnnotation": validateBool, 58 | "UnionTypeAnnotation": validateUnion, 59 | "VoidTypeAnnotation": (def) => joi.valid(void 8), 60 | "Array": validateArrayOf, 61 | } 62 | 63 | const validate = (value, type) => { 64 | if (!type) { 65 | throw new Error("Invalid type " + type); 66 | } 67 | 68 | return joi.validate(value, generateSchema(type), { 69 | abortEarly: false 70 | }); 71 | } 72 | 73 | return { generateSchema, validate }; 74 | } 75 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import serialize from "babel-literal-to-ast"; 2 | 3 | export default function ({types: t}) { 4 | function runtimeType(identifier, type) { 5 | const variable = t.variableDeclarator( 6 | t.identifier(`${identifier}Type`), 7 | serialize(type) 8 | ); 9 | return t.variableDeclaration("const", [variable]); 10 | } 11 | 12 | function parse(typeDef) { 13 | delete typeDef["start"]; 14 | delete typeDef["end"]; 15 | delete typeDef["loc"]; 16 | delete typeDef["extra"]; 17 | 18 | if (typeDef["id"]) { 19 | typeDef["id"] = parse(typeDef["id"]); 20 | } 21 | if (typeDef["key"]) { 22 | typeDef["key"] = parse(typeDef["key"]); 23 | } 24 | if (typeDef["value"]) { 25 | typeDef["value"] = parse(typeDef["value"]); 26 | } 27 | 28 | if (typeDef["types"]) { 29 | typeDef["types"] = typeDef["types"].map(parse); 30 | } 31 | if (typeDef["properties"]) { 32 | typeDef["properties"] = typeDef["properties"].map(parse); 33 | } 34 | 35 | return typeDef; 36 | } 37 | 38 | return { 39 | visitor: { 40 | TypeAlias(path) { 41 | const identifier = path.node.id.name; 42 | const parsed = parse(path.node.right); 43 | const variable = runtimeType(identifier, parsed); 44 | 45 | path.replaceWith( 46 | t.exportNamedDeclaration(variable, []) 47 | ); 48 | } 49 | } 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-transform-flow-introspection", 3 | "version": "0.0.1", 4 | "description": "Exports flowtype information at runtime for usage with testing/documentation libraries", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "Joe Stanton", 10 | "license": "ISC", 11 | "dependencies": { 12 | "astify": "0.0.4", 13 | "babel-core": "^6.17.0", 14 | "babel-literal-to-ast": "^0.1.2", 15 | "babel-plugin-syntax-flow": "^6.13.0", 16 | "babel-preset-es2015": "^6.16.0", 17 | "babel-register": "^6.16.3", 18 | "babel-runtime": "^6.11.6", 19 | "lodash.range": "^3.2.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /use-cases/generative-testing.js: -------------------------------------------------------------------------------- 1 | import * as Exports from "./types"; 2 | import {setup} from "../flow-runtime/gen"; 3 | 4 | const {gen, genN} = setup(Exports) // The generator must be able to recursively generate types; 5 | 6 | console.log(JSON.stringify(gen(Exports.LambdaContextType), null, 2)); 7 | console.log(JSON.stringify(gen(Exports.PropsType), null, 2)); 8 | console.log(JSON.stringify(gen(Exports.CardType), null, 2)); 9 | -------------------------------------------------------------------------------- /use-cases/index.js: -------------------------------------------------------------------------------- 1 | require("babel-register"); 2 | require("./generative-testing"); 3 | require("./validation"); 4 | -------------------------------------------------------------------------------- /use-cases/types.js: -------------------------------------------------------------------------------- 1 | type Props = { 2 | users: Array 3 | } 4 | 5 | type ProfilePic = { 6 | url: string, 7 | width: number, 8 | height: number 9 | } 10 | 11 | type Role = "Administrator" | "Editor" | "Reader"; 12 | type Administrator = "Administrator"; 13 | 14 | type User = { 15 | id: number, 16 | firstName: string, 17 | lastName: string, 18 | role: Role, 19 | activated: bool, 20 | profilePic: ProfilePic 21 | } 22 | 23 | type Event = { 24 | authToken: string, 25 | body: Object, 26 | params: Object, 27 | url: string, 28 | query: Object 29 | } 30 | 31 | type LambdaContext = { 32 | succeed?: (result: any) => void, 33 | fail: (error: Error) => void, 34 | done: (result: any) => void, 35 | getRemainingTimeInMillis: () => number, 36 | functionName?: string, 37 | functionVersion?: number | string, 38 | invokedFunctionArn?: string, 39 | memoryLimitInMB?: number, 40 | awsRequestId?: number | string, 41 | logGroupName?: string, 42 | logStreamName?: string 43 | }; 44 | 45 | type Message = { 46 | type: string, 47 | id: string 48 | } 49 | 50 | type WorkflowRule = { 51 | key: string, 52 | description: string, 53 | connection: Connection, 54 | path: string, 55 | if?: Function 56 | } 57 | 58 | type Connection = { 59 | baseUrl: string, 60 | headers?: Object 61 | } 62 | 63 | type Activity = { 64 | type: string, 65 | id: string, 66 | description: string, 67 | status: string, 68 | summary: string 69 | } 70 | 71 | type Query = { 72 | KeyConditionExpression: string, 73 | ExpressionAttributeNames?: Object, 74 | ExpressionAttributeValues: Object 75 | } 76 | 77 | type Filter = { 78 | FilterExpression: string, 79 | ExpressionAttributeNames?: Object, 80 | ExpressionAttributeValues: Object 81 | } 82 | 83 | // Example function, for later use with autoTest 84 | 85 | const genHTML = ({id, firstName, lastName} : Props) => ( 86 | `
  • ${firstName} ${lastName}
  • ` 87 | ); 88 | 89 | // Exmaples with Enums 90 | 91 | type Suit = 92 | | "Diamonds" 93 | | "Clubs" 94 | | "Hearts" 95 | | "Spades"; 96 | 97 | type Rank = 98 | | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 99 | | "Jack" 100 | | "Queen" 101 | | "King" 102 | | "Ace"; 103 | 104 | type Card = { 105 | suit: Suit, 106 | rank: Rank, 107 | } 108 | -------------------------------------------------------------------------------- /use-cases/validation.js: -------------------------------------------------------------------------------- 1 | import * as Exports from "./types"; 2 | 3 | import {setup} from "../flow-runtime/validation"; 4 | const {validate} = setup(Exports); // The validator must be able to recursively validate types 5 | 6 | console.log(validate("Diamond", Exports.SuitType)); 7 | console.log(validate("Administrator", Exports.AdministratorType)); 8 | console.log(validate({ 9 | "users": [ 10 | { 11 | "id": 1, 12 | "firstName": "Joe", 13 | "lastName": "String", 14 | "role": "Administrator", 15 | "activated": false, 16 | "profilePic": { 17 | "url": "String", 18 | "width": 1, 19 | "height": 1 20 | } 21 | } 22 | ] 23 | }, Exports.PropsType)); 24 | --------------------------------------------------------------------------------