Feuer frei!

master
Paul Loyd 2017-10-29 01:55:39 +03:00
commit 27629263c4
8 changed files with 411 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules/

30
bin/flow2avro Executable file
View File

@ -0,0 +1,30 @@
#!/usr/bin/env node
const fs = require('fs');
const argv = require('optimist')
.usage('Usage: $0 <path> ...')
.argv;
const generate = require('..');
argv._.forEach(run);
function run(path) {
if (path === '-') {
path = '/dev/stdin';
}
try {
const code = fs.readFileSync(path, 'utf8');
const schemes = generate(code);
console.dir(schemes, {
colors: true,
depth: Infinity,
});
} catch (ex) {
console.error(ex.message);
console.error(ex.stack);
}
}

39
lib/index.js Normal file
View File

@ -0,0 +1,39 @@
'use strict';
const assert = require('assert');
const parse = require('./parser');
const visit = require('./visitor');
const make = require('./maker');
function collect(node, schemes) {
const scheme = make(node);
const name = node.id.name;
assert(!schemes[name]);
scheme.name = name;
schemes[name] = scheme;
return false;
}
const visitor = {
TypeAlias: collect,
InterfaceDeclaration: collect,
ClassDeclaration: collect,
};
function generate(code) {
const ast = parse(code);
const schemes = Object.create({});
visit(ast, visitor, schemes);
return schemes;
}
module.exports = generate;

229
lib/maker.js Normal file
View File

@ -0,0 +1,229 @@
'use strict';
const assert = require('assert');
const {partition, log} = require('./utils');
function make(node) {
if (!handlers[node.type]) {
log(node);
return null;
}
return handlers[node.type](node);
}
const handlers = {
TypeAlias(node) {
return make(node.right);
},
InterfaceDeclaration(node) {
return make(node.body);
},
ClassDeclaration(node) {
return make(node.body);
},
ClassBody(node) {
return {
type: 'report',
fields: node.body.map(make).filter(Boolean),
};
},
ClassProperty(node) {
if (node.static) {
return null;
}
let type = node.leadingComments && getLastPragma(node.leadingComments);
return {
name: make(node.key),
type: type || make(node.typeAnnotation),
};
},
ClassMethod(node) {
return null;
},
ObjectTypeAnnotation(node) {
if (node.indexers.length > 0) {
// Allow functions, getters and setters.
const properties = node.properties.map(make).filter(Boolean);
assert.equal(properties.length, 0);
assert.equal(node.indexers.length, 1);
return {
type: 'map',
values: make(node.indexers[0]),
};
}
return {
type: 'record',
fields: node.properties.map(make).filter(Boolean),
};
},
ObjectTypeProperty(node) {
let type = null;
if (node.leadingComments) {
type = getLastPragma(node.leadingComments);
}
if (!type) {
type = make(node.value);
}
if (!type) {
return null;
}
return {
name: make(node.key),
type,
};
},
ObjectTypeIndexer(node) {
const key = make(node.key);
assert.equal(key, 'string');
return make(node.value);
},
TypeAnnotation(node) {
return make(node.typeAnnotation);
},
NumberTypeAnnotation(node) {
return 'double';
},
StringTypeAnnotation(node) {
return 'string';
},
BooleanTypeAnnotation(node) {
return 'boolean';
},
ArrayTypeAnnotation(node) {
return {
type: 'array',
items: make(node.elementType),
};
},
UnionTypeAnnotation(node) {
// TODO: flatten variants.
let [symbols, variants] = partition(node.types, isEnumSymbol);
symbols = symbols.map(unwrapEnumSymbol);
variants = variants.map(make);
if (symbols.length > 0) {
const enumeration = {
type: 'enum',
symbols,
};
if (variants.length === 0) {
return enumeration;
}
variants.push(enumeration);
}
return variants;
},
NullableTypeAnnotation(node) {
return ['null', make(node.typeAnnotation)];
},
NullLiteralTypeAnnotation(node) {
return {
type: 'null',
};
},
StringLiteralTypeAnnotation(node) {
return {
type: 'enum',
symbols: [node.value],
};
},
GenericTypeAnnotation(node) {
return make(node.id);
},
FunctionTypeAnnotation(node) {
return null;
},
Identifier(node) {
return node.name;
},
CommentLine(node) {
const marker = '$avro ';
const value = node.value.trimLeft();
if (value.startsWith(marker)) {
const pragma = value.slice(marker.length).trim();
assert(isValidPragma(pragma));
return pragma;
}
return null;
},
};
function isValidPragma(pragma) {
// TODO: support mixed.
switch (pragma) {
case 'null':
case 'int':
case 'long':
case 'float':
case 'double':
case 'bytes':
case 'string':
case 'boolean':
return true;
default:
return false;
}
}
function getLastPragma(comments) {
const pragmas = comments
.map(make)
.filter(Boolean);
return pragmas.length > 0 ? pragmas[pragmas.length - 1] : null;
}
function isEnumSymbol(node) {
return node.type === 'StringLiteralTypeAnnotation';
}
function unwrapEnumSymbol(node) {
return node.value;
}
module.exports = make;

16
lib/parser.js Normal file
View File

@ -0,0 +1,16 @@
'use strict';
const babylon = require('babylon');
function parse(code) {
// This parse configuration is intended to be as permissive as possible.
return babylon.parse(code, {
allowImportExportEverywhere: true,
allowReturnOutsideFunction: true,
allowSuperOutsideMethod: true,
sourceType: 'module',
plugins: [ '*', 'jsx', 'flow' ],
});
}
module.exports = parse;

19
lib/utils.js Normal file
View File

@ -0,0 +1,19 @@
function partition(iter, predicate) {
const left = [];
const right = [];
for (const item of iter) {
(predicate(item) ? left : right).push(item);
}
return [left, right];
}
function log(arg, depth = 5) {
console.dir(arg, {colors: true, depth});
}
module.exports = {
partition,
log,
};

46
lib/visitor.js Normal file
View File

@ -0,0 +1,46 @@
'use strict';
// Given the AST output of babylon parse, walk through in a depth-first order,
// calling methods on the given visitor, providing context as the first argument.
function visit(ast, visitor, state) {
let stack;
let parent;
let keys = [];
let index = -1;
do {
++index;
if (stack && index === keys.length) {
parent = stack.parent;
keys = stack.keys;
index = stack.index;
stack = stack.prev;
continue;
}
const node = parent ? parent[keys[index]] : ast.program;
if (node && typeof node === 'object' && (node.type || node.length)) {
const {type} = node;
if (type) {
if (type in visitor) {
const stop = visitor[type](node, state) === false;
if (stop) {
continue;
}
}
}
stack = { parent, keys, index, prev: stack };
parent = node;
keys = Object.keys(node);
index = -1;
}
} while (stack);
}
module.exports = visit;

31
package.json Normal file
View File

@ -0,0 +1,31 @@
{
"name": "flow2avro",
"version": "0.1.0",
"description": "Generate avro schemes for flowtype definitions",
"author": "Paul Loyd <pavelko95@gmail.com>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/loyd/flow2avro.git"
},
"bugs": {
"url": "https://github.com/loyd/flow2avro/issues"
},
"keywords": [
"flow",
"flowtype",
"avro",
"avsc"
],
"main": "lib/index.js",
"bin": {
"babylon": "./bin/babylon.js"
},
"dependencies": {
"babylon": "^6.18.0",
"optimist": "^0.6.1"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
}
}