From 921bdd04cb32dd9564865d32668eb7422d6eea2a Mon Sep 17 00:00:00 2001 From: Paul Loyd Date: Tue, 6 Nov 2018 20:03:36 +0300 Subject: [PATCH 1/2] Support string keys --- src/collector/definitions.js | 9 ++++++--- tests/samples/stringKeys/schema.json | 20 ++++++++++++++++++++ tests/samples/stringKeys/source.js | 13 +++++++++++++ tests/samples/stringKeys/types.yaml | 18 ++++++++++++++++++ 4 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 tests/samples/stringKeys/schema.json create mode 100644 tests/samples/stringKeys/source.js create mode 100644 tests/samples/stringKeys/types.yaml diff --git a/src/collector/definitions.js b/src/collector/definitions.js index 7e5eb0b..c4dd86a 100644 --- a/src/collector/definitions.js +++ b/src/collector/definitions.js @@ -12,7 +12,8 @@ import type { } from '@babel/types'; import { - isIdentifier, isObjectTypeProperty, isStringLiteralTypeAnnotation, isClassProperty, + isIdentifier, isStringLiteral, isObjectTypeProperty, + isStringLiteralTypeAnnotation, isClassProperty, } from '@babel/types'; import Context from './context'; @@ -215,10 +216,12 @@ function makeField(ctx: Context, node: ObjectTypeProperty | ClassProperty): ?Fie // TODO: warning about computed properties. invariant(isObjectTypeProperty(node) || !node.computed); - invariant(isIdentifier(node.key)); + invariant(isIdentifier(node.key) || isStringLiteral(node.key)); + + const name = isIdentifier(node.key) ? node.key.name : node.key.value; return { - name: node.key.name, + name, value: type, required: node.optional == null || !node.optional, }; diff --git a/tests/samples/stringKeys/schema.json b/tests/samples/stringKeys/schema.json new file mode 100644 index 0000000..346f76a --- /dev/null +++ b/tests/samples/stringKeys/schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "definitions": { + "stringKeys::X": { + "type": "object", + "properties": {"a b": {"type": "string"}}, + "required": ["a b"] + }, + "stringKeys::Y": { + "type": "object", + "properties": {"a b": {"type": "string"}}, + "required": ["a b"] + }, + "stringKeys::Z": { + "type": "object", + "properties": {"a b": {"type": "string"}}, + "required": ["a b"] + } + } +} diff --git a/tests/samples/stringKeys/source.js b/tests/samples/stringKeys/source.js new file mode 100644 index 0000000..ab89ecc --- /dev/null +++ b/tests/samples/stringKeys/source.js @@ -0,0 +1,13 @@ +type X = { + 'a b': string; +}; + +interface Y { + 'a b': string; +} + +class Z { + 'a b': string; +} + +export {X, Y, Z}; diff --git a/tests/samples/stringKeys/types.yaml b/tests/samples/stringKeys/types.yaml new file mode 100644 index 0000000..5b7505f --- /dev/null +++ b/tests/samples/stringKeys/types.yaml @@ -0,0 +1,18 @@ +- kind: record + fields: + - name: "a b" + value: {kind: string} + required: true + id: [stringKeys, X] +- kind: record + fields: + - name: "a b" + value: {kind: string} + required: true + id: [stringKeys, Y] +- kind: record + fields: + - name: "a b" + value: {kind: string} + required: true + id: [stringKeys, Z] From bc2e779b19de9755d706ab835ddc3f15464f81e4 Mon Sep 17 00:00:00 2001 From: Vladislav Kurkin Date: Tue, 20 Nov 2018 20:59:15 +0300 Subject: [PATCH 2/2] =?UTF-8?q?MARKETFRONTECH-701:=20JSON-=D1=81=D1=85?= =?UTF-8?q?=D0=B5=D0=BC=D0=B0.=20=D0=9F=D0=BE=D0=B4=D0=B4=D0=B5=D1=80?= =?UTF-8?q?=D0=B6=D0=B0=D1=82=D1=8C=20=D1=80=D0=B0=D1=81=D1=88=D0=B8=D1=80?= =?UTF-8?q?=D0=B5=D0=BD=D0=BD=D1=8B=D0=B5=20=D0=BF=D0=BE=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D1=81=D1=83=D1=89=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B5=D0=B9:=20kind=20,=20$id=20=D0=B8=20=D1=82.=D0=B4.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- declarations/babel.js | 9 +++ package.json | 5 +- src/collector/declarations.js | 9 +-- src/collector/definitions.js | 55 ++++++++++-------- src/collector/globals.js | 29 ++++++++++ src/collector/index.js | 18 +++--- src/collector/module.js | 4 +- src/collector/pragmas.js | 61 +++++++++++++------- src/collector/scope.js | 18 +++--- src/generators/jsonSchema.js | 95 ++++++++++++++++++++++++-------- src/index.js | 12 ++-- src/options.js | 7 +++ src/parser.js | 2 +- src/types.js | 2 + tests/samples/call/schema.json | 11 ++++ tests/samples/call/source.js | 7 +++ tests/samples/call/types.yaml | 8 +++ tests/samples/error/schema.json | 20 +++++++ tests/samples/error/source.js | 11 ++++ tests/samples/error/types.yaml | 20 +++++++ tests/samples/number/schema.json | 20 +++++++ tests/samples/number/source.js | 11 ++++ tests/samples/number/types.yaml | 20 +++++++ tests/samples/string/schema.json | 20 +++++++ tests/samples/string/source.js | 11 ++++ tests/samples/string/types.yaml | 20 +++++++ 26 files changed, 408 insertions(+), 97 deletions(-) create mode 100644 src/options.js create mode 100644 tests/samples/call/schema.json create mode 100644 tests/samples/call/source.js create mode 100644 tests/samples/call/types.yaml create mode 100644 tests/samples/error/schema.json create mode 100644 tests/samples/error/source.js create mode 100644 tests/samples/error/types.yaml create mode 100644 tests/samples/number/schema.json create mode 100644 tests/samples/number/source.js create mode 100644 tests/samples/number/types.yaml create mode 100644 tests/samples/string/schema.json create mode 100644 tests/samples/string/source.js create mode 100644 tests/samples/string/types.yaml diff --git a/declarations/babel.js b/declarations/babel.js index 07cb6bb..40822f1 100644 --- a/declarations/babel.js +++ b/declarations/babel.js @@ -584,6 +584,14 @@ declare module '@babel/types' { body: ObjectTypeAnnotation; } + declare class OpaqueType extends Node { + type: 'OpaqueType'; + id: Identifier; + typeParameters: TypeParameterDeclaration | null; + supertype: FlowTypeAnnotation | null; + impltype: FlowTypeAnnotation; + } + declare class DeclareFunction extends Node { type: 'DeclareFunction'; id: Identifier; @@ -1169,6 +1177,7 @@ declare module '@babel/types' { declare function isArrowFunctionExpression(node: mixed, opts: Object | void): boolean %checks (node instanceof ArrowFunctionExpression); declare function isClassBody(node: mixed, opts: Object | void): boolean %checks (node instanceof ClassBody); declare function isClassDeclaration(node: mixed, opts: Object | void): boolean %checks (node instanceof ClassDeclaration); + declare function isOpaqueType(node: mixed, opts: Object | void): boolean %checks (node instanceof OpaqueType); declare function isClassExpression(node: mixed, opts: Object | void): boolean %checks (node instanceof ClassExpression); declare function isExportAllDeclaration(node: mixed, opts: Object | void): boolean %checks (node instanceof ExportAllDeclaration); declare function isExportDefaultDeclaration(node: mixed, opts: Object | void): boolean %checks (node instanceof ExportDefaultDeclaration); diff --git a/package.json b/package.json index a6e1eaa..6858dcc 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "flow2schema", + "name": "@yandex-market/flow2schema", "version": "0.3.2", "description": "Generate schemas for flowtype definitions", "author": "Paul Loyd ", @@ -53,5 +53,8 @@ "build": "babel src/ -d lib/", "mocha": "nyc mocha -r @babel/register -R list tests/run.js", "flow": "flow" + }, + "publishConfig": { + "registry": "http://npm.yandex-team.ru/" } } diff --git a/src/collector/declarations.js b/src/collector/declarations.js index df22ccc..a04ac90 100644 --- a/src/collector/declarations.js +++ b/src/collector/declarations.js @@ -7,7 +7,7 @@ import type { Block, ClassDeclaration, ExportDefaultDeclaration, ExportNamedDeclaration, Identifier, ImportDeclaration, ImportDefaultSpecifier, ImportSpecifier, InterfaceDeclaration, Node, TypeAlias, TypeParameterDeclaration, VariableDeclaration, VariableDeclarator, - DeclareTypeAlias, DeclareInterface, DeclareClass, + DeclareTypeAlias, DeclareInterface, DeclareClass, OpaqueType, } from '@babel/types'; import { @@ -15,9 +15,9 @@ import { isExportNamedDeclaration, isIdentifier, isImportDeclaration, isImportNamespaceSpecifier, isImportSpecifier, isInterfaceDeclaration, isObjectPattern, isObjectProperty, isDeclareClass, isStringLiteral, isTypeAlias, isVariableDeclaration, isDeclareTypeAlias, isDeclareInterface, + isOpaqueType, } from '@babel/types'; -import {invariant} from '../utils'; import Context from './context'; import type {ExternalInfo, TemplateParam} from './query'; @@ -198,11 +198,12 @@ function processExportDefaultDeclaration(ctx: Context, node: ExportDefaultDeclar */ type Declaration = TypeAlias | InterfaceDeclaration | ClassDeclaration - | DeclareTypeAlias | DeclareInterface | DeclareClass; + | DeclareTypeAlias | DeclareInterface | DeclareClass | OpaqueType; function isDeclaration(node: mixed): boolean %checks { return isTypeAlias(node) || isInterfaceDeclaration(node) || isClassDeclaration(node) - || isDeclareTypeAlias(node) || isDeclareInterface(node) || isDeclareClass(node); + || isDeclareTypeAlias(node) || isDeclareInterface(node) || isDeclareClass(node) + || isOpaqueType(node); } function processDeclaration(ctx: Context, node: Declaration) { diff --git a/src/collector/definitions.js b/src/collector/definitions.js index 56fe4ba..e7cbb3b 100644 --- a/src/collector/definitions.js +++ b/src/collector/definitions.js @@ -8,7 +8,7 @@ import type { GenericTypeAnnotation, InterfaceDeclaration, IntersectionTypeAnnotation, TypeAlias, UnionTypeAnnotation, NullableTypeAnnotation, ObjectTypeIndexer, ObjectTypeProperty, StringLiteralTypeAnnotation, ObjectTypeAnnotation, AnyTypeAnnotation, MixedTypeAnnotation, - TupleTypeAnnotation, DeclareTypeAlias, DeclareInterface, DeclareClass, + TupleTypeAnnotation, DeclareTypeAlias, DeclareInterface, DeclareClass, OpaqueType, } from '@babel/types'; import { @@ -19,8 +19,7 @@ import { import Context from './context'; import type { - Type, RecordType, Field, ArrayType, TupleType, MapType, UnionType, IntersectionType, - MaybeType, NumberType, StringType, BooleanType, LiteralType, ReferenceType, + Type, RecordType, Field, ArrayType, TupleType, MapType, MaybeType, } from '../types'; import * as t from '../types'; @@ -39,6 +38,16 @@ function processTypeAlias(ctx: Context, node: TypeAlias | DeclareTypeAlias) { ctx.define(name, type); } +function processOpaqueType(ctx: Context, node: OpaqueType) { + const {name} = node.id; + // TODO: processing supertype annotation. + const type = makeType(ctx, node.impltype); + + invariant(type); + + ctx.define(name, type); +} + // TODO: type params. function processInterfaceDeclaration( ctx: Context, @@ -59,10 +68,17 @@ function processInterfaceDeclaration( for (const extend of node.extends) { const {name} = extend.id; const type = ctx.query(name); + let reference; - invariant(type && type.id); + invariant(type); - const reference = t.createReference(t.clone(type.id)); + if (type.kind === 'reference') { + reference = type; + } else { + invariant(type.id); + + reference = t.createReference(t.clone(type.id)); + } parts.push(reference); } @@ -138,6 +154,8 @@ function makeType(ctx: Context, node: FlowTypeAnnotation): ?Type { return t.createAny(); case 'MixedTypeAnnotation': return t.createMixed(); + case 'TypeofTypeAnnotation': + return t.createAny(); case 'FunctionTypeAnnotation': return null; default: @@ -190,27 +208,19 @@ function makeField(ctx: Context, node: ObjectTypeProperty | ClassProperty): ?Fie return null; } - let type = null; + const value = isObjectTypeProperty(node) ? node.value : node.typeAnnotation; + + // TODO: no type annotation for the class property. + invariant(value); + + let type = makeType(ctx, value); if (node.leadingComments) { - const pragma = (wu: $FlowIssue<4431>)(node.leadingComments) + type = (wu: $FlowIssue<4431>)(node.leadingComments) .pluck('value') - .map(extractPragmas) + .map(line => extractPragmas(type, line)) .flatten() - .find(pragma => pragma.kind === 'type'); - - if (pragma) { - type = pragma.value; - } - } - - if (!type) { - const value = isObjectTypeProperty(node) ? node.value : node.typeAnnotation; - - // TODO: no type annotation for the class property. - invariant(value); - - type = makeType(ctx, value); + .toArray()[0]; } if (!type) { @@ -303,4 +313,5 @@ export default { DeclareInterface: processInterfaceDeclaration, ClassDeclaration: processClassDeclaration, DeclareClass: processInterfaceDeclaration, + OpaqueType: processOpaqueType, } diff --git a/src/collector/globals.js b/src/collector/globals.js index 3b17194..85d7cf2 100644 --- a/src/collector/globals.js +++ b/src/collector/globals.js @@ -28,6 +28,27 @@ function array(params: (?Type)[]): ?Type { return t.createArray(t.clone(params[0])); } +// Error. +function error(params: (?Type)[]): ?Type { + invariant(params.length === 0); + + return t.createReference(['Error']); +} + +// String. +function string(params: (?Type)[]): ?Type { + invariant(params.length === 0); + + return t.createReference(['String']); +} + +// Number. +function number(params: (?Type)[]): ?Type { + invariant(params.length === 0); + + return t.createReference(['Number']); +} + // $ElementType and $PropertyType. function elemType(params: (?Type)[], resolve: TypeId => Type): ?Type { invariant(params.length === 2); @@ -233,10 +254,17 @@ function extDef(params: (?Type)[]): ?Type { return type; } +function call(): ?Type { + return t.createAny(); +} + export default { Object: object, Buffer: buffer, Array: array, + Error: error, + String: string, + Number: number, $ReadOnlyArray: array, $PropertyType: elemType, $ElementType: elemType, @@ -250,6 +278,7 @@ export default { $All: all, $Either: either, $FlowFixMe: fixMe, + $Call: call, // Extends types $$extDef: extDef, diff --git a/src/collector/index.js b/src/collector/index.js index e7ed6ae..8c16219 100644 --- a/src/collector/index.js +++ b/src/collector/index.js @@ -2,7 +2,6 @@ import * as fs from 'fs'; import * as pathlib from 'path'; -import wu from 'wu'; import {isNode} from '@babel/types'; import type {Node} from '@babel/types'; @@ -17,20 +16,21 @@ import Context from './context'; import {invariant} from '../utils'; import type Parser from '../parser'; import type {Type, TypeId} from '../types'; +import type {Options} from '../options'; import type {TemplateParam} from './query'; const VISITOR = Object.assign({}, definitionGroup, declarationGroup); export default class Collector { - +root: string; +parser: Parser; + +options: Options; _fund: Fund; _modules: Map; _global: Scope; - constructor(parser: Parser, root: string = '.') { - this.root = root; + constructor(parser: Parser, options?: Options = {}) { this.parser = parser; + this.options = options; this._fund = new Fund; this._modules = new Map; this._global = Scope.global(globals); @@ -38,21 +38,23 @@ export default class Collector { collect(path: string, internal: boolean = false) { // TODO: follow symlinks. - path = pathlib.resolve(path); - let module = this._modules.get(path); if (module) { return; } + const lib = this.options.lib; + const libPath: string = lib && lib[path] || path; + // TODO: error wrapping. - const code = fs.readFileSync(path, 'utf8'); + const code = fs.readFileSync(libPath, 'utf8'); const ast = this.parser.parse(code); // TODO: customize it. // XXX: replace with normal resolver and path-to-id converter. const id = pathToId(path.replace(/source\.js$/, '')); + module = new Module(id, path); const scope = this._global.extend(module); @@ -104,7 +106,7 @@ export default class Collector { switch (result.kind) { case 'external': - const modulePath = scope.resolve(result.info.path); + const modulePath = scope.resolve(result.info.path, this.options.sourceModuleExtensions); this.collect(modulePath, true); diff --git a/src/collector/module.js b/src/collector/module.js index 18e9ac1..9dc95cd 100644 --- a/src/collector/module.js +++ b/src/collector/module.js @@ -47,11 +47,11 @@ export default class Module { return scope.query(reference, params); } - resolve(path: string): string { + resolve(path: string, extensions: ?string[]): string { const basedir = pathlib.dirname(this.path); // TODO: follow symlinks. - return resolve.sync(path, {basedir}); + return resolve.sync(path, {basedir, extensions: (extensions || []).concat(['.js'])}); } exports(): Iterator<[Scope, string]> { diff --git a/src/collector/pragmas.js b/src/collector/pragmas.js index 3bc956f..2219e20 100644 --- a/src/collector/pragmas.js +++ b/src/collector/pragmas.js @@ -1,35 +1,54 @@ // @flow +import {ClassProperty, isObjectTypeProperty, ObjectTypeProperty} from '@babel/types'; + import {invariant} from '../utils'; import type {Type} from '../types'; import {createNumber, isRepr} from '../types'; -export type Pragma = - | TypePragma - ; - -export type TypePragma = { - kind: 'type', - value: Type, -}; +const PRAGMA_RE = /^\s*@(.+?)\s+{\s*(.+?)\s*}\s*$/gm; -const PRAGMA_RE = /^\s*@repr\s+\{\s*(.+?)\s*\}\s*$/gm; - -export function extractPragmas(text: string): Pragma[] { - const pragmas = []; +export function extractPragmas(type: ?Type, text: string): ?Type { let match; while ((match = PRAGMA_RE.exec(text))) { - const repr = match[1]; - - invariant(isRepr(repr)); - - pragmas.push({ - kind: 'type', - value: createNumber(repr), - }); + type = processingPragma(type, match[1], match[2]); } - return pragmas; + return type; } + +const processingPragma = (type: ?Type, name: string, value: string): ?Type => { + switch (name) { + case 'repr': + invariant(isRepr(value)); + + return createNumber(value); + case 'description': + case 'title': + case 'maxProperties': + case 'minProperties': + case 'patternProperties': + case 'maxItems': + case 'minItems': + case 'uniqueItems': + case 'multipleOf': + case 'maximum': + case 'exclusiveMaximum': + case 'minimum': + case 'exclusiveMinimum': + case 'maxLength': + case 'minLength': + case 'pattern': + case 'format': + if (type) { + // $FlowFixMe + type[name] = value; + } + + return type; + default: + throw `Unknown pragma: ${name}`; + } +}; diff --git a/src/collector/scope.js b/src/collector/scope.js index 2b212cf..5980a26 100644 --- a/src/collector/scope.js +++ b/src/collector/scope.js @@ -114,14 +114,14 @@ export default class Scope { this.module.addExport(name, this, reference); } - resolve(path: string): string { + resolve(path: string, extensions: ?string[]): string { invariant(this.module); - return this.module.resolve(path); + return this.module.resolve(path, extensions); } query(name: string, params: (?Type)[]): Query { - const entry = this._entries.get(name); + let entry = this._entries.get(name); if (entry && entry.kind === 'template') { const augmented = entry.params.map((p, i) => params[i] === undefined ? p.value : params[i]); @@ -136,17 +136,15 @@ export default class Scope { } } - if (entry) { - return entry; + if (!entry && this.module) { + entry = this.module.query(name, params); } - if (this.parent) { - return this.parent.query(name, params); + if ((!entry || entry.kind === 'unknown') && this.parent) { + entry = this.parent.query(name, params); } - return { - kind: 'unknown', - }; + return entry || {kind: 'unknown'}; } } diff --git a/src/generators/jsonSchema.js b/src/generators/jsonSchema.js index 57dc582..26b75d9 100644 --- a/src/generators/jsonSchema.js +++ b/src/generators/jsonSchema.js @@ -5,6 +5,7 @@ import wu from 'wu'; import {invariant, collect, partition} from '../utils'; import type Fund from '../fund'; import type {Type} from '../types'; +import type {Options} from '../options'; export type SchemaType = 'object' | 'array' | 'boolean' | 'integer' | 'number' | 'string' | 'null'; @@ -44,17 +45,19 @@ export type Schema = boolean | { not?: Schema, }; -function convert(fund: Fund, type: ?Type): Schema { +function convert(fund: Fund, type: ?Type, options: ?Options): Schema { if (!type) { return { type: 'null', }; } + const {title, description} = type; + switch (type.kind) { case 'record': const properties = collect( - wu(type.fields).map(field => [field.name, convert(fund, field.value)]) + wu(type.fields).map(field => [field.name, convert(fund, field.value, options)]) ); const required = wu(type.fields) @@ -63,33 +66,47 @@ function convert(fund: Fund, type: ?Type): Schema { .toArray(); //ToDo: support patternProperties - const {maxProperties, minProperties, propertyNames} = type; - const additionalProperties = type.additionalProperties && convert(fund, type.additionalProperties); + let {maxProperties, minProperties, propertyNames} = type; + const additionalProperties = type.additionalProperties && convert(fund, type.additionalProperties, options); return required.length > 0 ? { type: 'object', properties, required, - ...clearType({maxProperties, minProperties, additionalProperties, propertyNames}), + ...clearType({ + title, + description, + maxProperties, + minProperties, + additionalProperties, + propertyNames, + }), } : { type: 'object', properties, - ...clearType({maxProperties, minProperties, additionalProperties, propertyNames}), + ...clearType({ + title, + description, + maxProperties, + minProperties, + additionalProperties, + propertyNames, + }), }; case 'array': const {maxItems, minItems, uniqueItems} = type; - const additionalItems = type.additionalItems && convert(fund, type.additionalItems); - const contains = type.contains && convert(fund, type.contains); + const additionalItems = type.additionalItems && convert(fund, type.additionalItems, options); + const contains = type.contains && convert(fund, type.contains, options); return { type: 'array', - items: convert(fund, type.items), + items: convert(fund, type.items, options), ...clearType({additionalItems, contains, maxItems, minItems, uniqueItems}), }; case 'tuple': return { type: 'array', - items: wu(type.items).map(type => convert(fund, type)).toArray(), + items: wu(type.items).map(type => convert(fund, type, options)).toArray(), }; case 'map': // TODO: invariant(type.keys.kind === 'string'); @@ -97,7 +114,7 @@ function convert(fund: Fund, type: ?Type): Schema { return { type: 'object', - additionalProperties: convert(fund, type.values), + additionalProperties: convert(fund, type.values, options), }; case 'union': const enumerate = wu(type.variants) @@ -108,7 +125,7 @@ function convert(fund: Fund, type: ?Type): Schema { const schemas = wu(type.variants) .filter(variant => variant.kind !== 'literal') - .map(variant => convert(fund, variant)) + .map(variant => convert(fund, variant, options)) .toArray(); if (schemas.length === 0) { @@ -129,10 +146,10 @@ function convert(fund: Fund, type: ?Type): Schema { case 'intersection': const [maps, others] = partition(type.parts, type => type.kind === 'map'); - const parts = wu(others).map(part => convert(fund, part)).toArray(); + const parts = wu(others).map(part => convert(fund, part, options)).toArray(); if (maps.length > 0) { - const keys = wu(maps).map(map => convert(fund, (map: $FlowFixMe).values)).toArray(); + const keys = wu(maps).map(map => convert(fund, (map: $FlowFixMe).values, options)).toArray(); const key = keys.length === 1 ? keys[0] : {anyOf: keys}; if (parts.length === 1 && parts[0].type === 'object') { @@ -153,23 +170,45 @@ function convert(fund: Fund, type: ?Type): Schema { }; case 'maybe': return { - anyOf: [convert(fund, type.value), {type: 'null'}], + anyOf: [convert(fund, type.value, options), {type: 'null'}], }; case 'number': - const {multipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum} = type; + let { + multipleOf, + maximum, + exclusiveMaximum, + minimum, + exclusiveMinimum + } = type; switch (type.repr) { case 'f32': case 'f64': return { type: 'number', - ...clearType({multipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum}), + ...clearType({ + title, + description, + multipleOf, + maximum, + exclusiveMaximum, + minimum, + exclusiveMinimum, + }), }; case 'i32': case 'i64': return { type: 'integer', - ...clearType({multipleOf, maximum, exclusiveMaximum, minimum, exclusiveMinimum}), + ...clearType({ + title, + description, + multipleOf, + maximum, + exclusiveMaximum, + minimum, + exclusiveMinimum, + }), }; default: return { @@ -178,11 +217,18 @@ function convert(fund: Fund, type: ?Type): Schema { }; } case 'string': - const {maxLength, minLength, pattern, format} = type; + let {maxLength, minLength, pattern, format} = type; return { type: 'string', - ...clearType({maxLength, minLength, pattern, format}), + ...clearType({ + title, + description, + maxLength, + minLength, + pattern, + format, + }), }; case 'boolean': return { @@ -201,8 +247,10 @@ function convert(fund: Fund, type: ?Type): Schema { return true; case 'reference': default: + const separator = options && options.referenceSchemaSeparator || '::'; + return { - $ref: `#/definitions/${type.to.join('::')}`, + $ref: `#/definitions/${type.to.join(separator)}`, }; } } @@ -217,11 +265,12 @@ const clearType = (type: {[string]: mixed}) => { return type; }; -export default function (fund: Fund): Schema { +export default function (fund: Fund, options: ?Options): Schema { + const separator = options && options.referenceSchemaSeparator || '::'; const schemas = wu(fund.takeAll()).map(type => { invariant(type.id); - return [type.id.join('::'), convert(fund, type)]; + return [type.id.join(separator), convert(fund, type, options)]; }); return { diff --git a/src/index.js b/src/index.js index 5cc07d3..84a221a 100644 --- a/src/index.js +++ b/src/index.js @@ -3,15 +3,17 @@ import Parser from './parser'; import Collector from './collector'; import type {Type} from './types'; -import generateJsonSchema from './generators/jsonSchema'; -import type {Schema} from './generators/jsonSchema'; +import generateJsonSchema, {type Schema} from './generators/jsonSchema'; +import type {Options} from './options'; + +export type $$extDef = () => T; // @see babel#6805. //export {Parser, Collector}; -function collect(path: string): {+types: Type[], +schema: Schema} { +function collect(path: string, options?: Options): {+types: Type[], +schema: Schema} { const parser = new Parser; - const collector = new Collector(parser); + const collector = new Collector(parser, options); collector.collect(path); @@ -19,7 +21,7 @@ function collect(path: string): {+types: Type[], +schema: Schema} { return { types: Array.from(fund.takeAll()), - schema: generateJsonSchema(fund), + schema: generateJsonSchema(fund, options), }; } diff --git a/src/options.js b/src/options.js new file mode 100644 index 0000000..034668a --- /dev/null +++ b/src/options.js @@ -0,0 +1,7 @@ +// @flow + +export type Options = { + sourceModuleExtensions?: string[], + referenceSchemaSeparator?: string, + lib?: {[string]: string}, +}; diff --git a/src/parser.js b/src/parser.js index d270aab..772f2d6 100644 --- a/src/parser.js +++ b/src/parser.js @@ -14,7 +14,7 @@ export default class Parser { allowSuperOutsideMethod: true, sourceType: 'module', // TODO: review other plugins. - plugins: ['*', 'jsx', 'flow', 'classProperties'], + plugins: ['*', 'jsx', 'flow', 'classProperties', 'objectRestSpread'], }); } } diff --git a/src/types.js b/src/types.js index 9ea887a..2c45294 100644 --- a/src/types.js +++ b/src/types.js @@ -21,6 +21,8 @@ export type TypeId = string[]; export type BaseType = { id?: TypeId, + title?: string, + description?: string, }; export type RecordType = BaseType & { diff --git a/tests/samples/call/schema.json b/tests/samples/call/schema.json new file mode 100644 index 0000000..31bc044 --- /dev/null +++ b/tests/samples/call/schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "definitions": { + "call::X": true, + "call::Y": { + "type": "object", + "properties": {"y": true}, + "required": ["y"] + } + } +} diff --git a/tests/samples/call/source.js b/tests/samples/call/source.js new file mode 100644 index 0000000..949bdc1 --- /dev/null +++ b/tests/samples/call/source.js @@ -0,0 +1,7 @@ +type X = $Call; + +type Y = { + y: $Call; +}; + +export {X, Y}; diff --git a/tests/samples/call/types.yaml b/tests/samples/call/types.yaml new file mode 100644 index 0000000..253aa83 --- /dev/null +++ b/tests/samples/call/types.yaml @@ -0,0 +1,8 @@ +- kind: any + id: [call, X] +- kind: record + fields: + - name: y + value: {kind: any} + required: true + id: [call, Y] diff --git a/tests/samples/error/schema.json b/tests/samples/error/schema.json new file mode 100644 index 0000000..6a06ace --- /dev/null +++ b/tests/samples/error/schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "definitions": { + "error::X": {"$ref": "#/definitions/Error"}, + "error::Y": { + "type": "object", + "properties": { + "y": {"$ref": "#/definitions/Error"} + }, + "required": ["y"] + }, + "error::Z": { + "type": "object", + "properties": { + "z": {"$ref": "#/definitions/Error"} + }, + "required": ["z"] + } + } +} diff --git a/tests/samples/error/source.js b/tests/samples/error/source.js new file mode 100644 index 0000000..a3d7a42 --- /dev/null +++ b/tests/samples/error/source.js @@ -0,0 +1,11 @@ +type X = Error; + +interface Y { + y: Error; +} + +class Z { + z: Error; +} + +export {X, Y, Z}; diff --git a/tests/samples/error/types.yaml b/tests/samples/error/types.yaml new file mode 100644 index 0000000..bedffec --- /dev/null +++ b/tests/samples/error/types.yaml @@ -0,0 +1,20 @@ +- + kind: reference + to: [Error] + id: [error, X] +- kind: record + fields: + - name: y + value: + kind: reference + to: [Error] + required: true + id: [error, Y] +- kind: record + fields: + - name: z + value: + kind: reference + to: [Error] + required: true + id: [error, Z] diff --git a/tests/samples/number/schema.json b/tests/samples/number/schema.json new file mode 100644 index 0000000..39f46e5 --- /dev/null +++ b/tests/samples/number/schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "definitions": { + "number::X": {"$ref": "#/definitions/Number"}, + "number::Y": { + "type": "object", + "properties": { + "y": {"$ref": "#/definitions/Number"} + }, + "required": ["y"] + }, + "number::Z": { + "type": "object", + "properties": { + "z": {"$ref": "#/definitions/Number"} + }, + "required": ["z"] + } + } +} diff --git a/tests/samples/number/source.js b/tests/samples/number/source.js new file mode 100644 index 0000000..ad9743f --- /dev/null +++ b/tests/samples/number/source.js @@ -0,0 +1,11 @@ +type X = Number; + +interface Y { + y: Number; +} + +class Z { + z: Number; +} + +export {X, Y, Z}; diff --git a/tests/samples/number/types.yaml b/tests/samples/number/types.yaml new file mode 100644 index 0000000..8995ad8 --- /dev/null +++ b/tests/samples/number/types.yaml @@ -0,0 +1,20 @@ +- + kind: reference + to: [Number] + id: [number, X] +- kind: record + fields: + - name: y + value: + kind: reference + to: [Number] + required: true + id: [number, Y] +- kind: record + fields: + - name: z + value: + kind: reference + to: [Number] + required: true + id: [number, Z] diff --git a/tests/samples/string/schema.json b/tests/samples/string/schema.json new file mode 100644 index 0000000..0fa6852 --- /dev/null +++ b/tests/samples/string/schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-06/schema#", + "definitions": { + "string::X": {"$ref": "#/definitions/String"}, + "string::Y": { + "type": "object", + "properties": { + "y": {"$ref": "#/definitions/String"} + }, + "required": ["y"] + }, + "string::Z": { + "type": "object", + "properties": { + "z": {"$ref": "#/definitions/String"} + }, + "required": ["z"] + } + } +} diff --git a/tests/samples/string/source.js b/tests/samples/string/source.js new file mode 100644 index 0000000..45ea64b --- /dev/null +++ b/tests/samples/string/source.js @@ -0,0 +1,11 @@ +type X = String; + +interface Y { + y: String; +} + +class Z { + z: String; +} + +export {X, Y, Z}; diff --git a/tests/samples/string/types.yaml b/tests/samples/string/types.yaml new file mode 100644 index 0000000..5e3e032 --- /dev/null +++ b/tests/samples/string/types.yaml @@ -0,0 +1,20 @@ +- + kind: reference + to: [String] + id: [string, X] +- kind: record + fields: + - name: y + value: + kind: reference + to: [String] + required: true + id: [string, Y] +- kind: record + fields: + - name: z + value: + kind: reference + to: [String] + required: true + id: [string, Z]