diff --git a/src/fund.js b/src/fund.js index b7b9fa4..c7cbe7d 100644 --- a/src/fund.js +++ b/src/fund.js @@ -28,7 +28,11 @@ export default class Fund { return type; } - flatten(): Type[] { - return Array.from(this._types.values()); + takeAll(): Iterable { + return this._types.values(); + } + + takeTops(): Iterable { + return this._tops; } } diff --git a/src/generators/jsonSchema.js b/src/generators/jsonSchema.js new file mode 100644 index 0000000..a450d25 --- /dev/null +++ b/src/generators/jsonSchema.js @@ -0,0 +1,166 @@ +import wu from 'wu'; + +import {invariant, collect} from '../utils'; +import type Fund from '../fund'; +import type {Type, NumberType} from '../types'; + +export type SchemaType = 'object' | 'array' | 'boolean' | 'integer' | 'number' | 'string' | 'null'; + +export type Schema = { + id?: string, + $ref?: string, + $schema?: string, + title?: string, + description?: string, + default?: mixed, + multipleOf?: number, + maximum?: number, + exclusiveMaximum?: boolean, + minimum?: number, + exclusiveMinimum?: boolean, + maxLength?: number, + minLength?: number, + pattern?: string, + additionalItems?: boolean | Schema, + items?: Schema | Schema[], + maxItems?: number, + minItems?: number, + uniqueItems?: boolean, + maxProperties?: number, + minProperties?: number, + required?: string[], + additionalProperties?: boolean | Schema, + definitions?: {[string]: Schema}, + properties?: {[string]: Schema}, + patternProperties?: {[string]: Schema}, + dependencies?: {[string]: Schema | string[]}, + enum?: mixed[], + type?: SchemaType | SchemaType[], + allOf?: Schema[], + anyOf?: Schema[], + oneOf?: Schema[], + not?: Schema, +}; + +function convert(fund: Fund, type: ?Type): Schema { + if (!type) { + return { + type: 'null', + }; + } + + switch (type.kind) { + case 'record': + const properties = collect( + wu(type.fields).map(field => [field.name, convert(fund, field.value)]) + ); + + const required = wu(type.fields) + .filter(field => field.required) + .pluck('name') + .toArray(); + + const schema: Schema = { + type: 'object', + properties, + }; + + if (required.length > 0) { + schema.required = required; + } + + return schema; + case 'array': + return { + type: 'array', + items: convert(fund, type.items), + }; + case 'tuple': + return { + type: 'array', + items: wu(type.items).map(type => convert(fund, type)).toArray(), + }; + case 'map': + // TODO: invariant(type.keys.kind === 'string'); + + return { + type: 'object', + additionalProperties: convert(fund, type.values), + }; + case 'union': + const enumerate = wu(type.variants) + .filter(variant => variant.kind === 'literal') + .map(literal => (literal: $FlowFixMe).value) + .tap(value => invariant(value !== undefined)) + .toArray(); + + const schemas = wu(type.variants) + .filter(variant => variant.kind !== 'literal') + .map(variant => convert(fund, variant)) + .toArray(); + + if (schemas.length === 0) { + return { + enum: enumerate, + }; + } + + if (enumerate.length > 0) { + schemas.push({ + enum: enumerate, + }); + } + + return { + anyOf: schemas, + }; + case 'intersection': + return { + allOf: wu(type.parts).map(part => convert(fund, part)).toArray(), + }; + case 'maybe': + return { + oneOf: [convert(fund, type.value), {type: 'null'}], + }; + case 'number': + return { + type: type.repr === 'f32' || type.repr === 'f64' ? 'number' : 'integer', + }; + case 'string': + return { + type: 'string', + }; + case 'boolean': + return { + type: 'boolean', + }; + case 'literal': + invariant(type.value !== undefined); + + return type.value === null ? { + type: 'null', + } : { + enum: [type.value], + }; + case 'any': + case 'mixed': + return {}; + case 'reference': + default: + return { + $ref: `#/definitions/${type.to.join('::')}`, + }; + } +} + +export default function (fund: Fund): Schema { + const schemas = wu(fund.takeAll()).map(type => { + invariant(type.id); + + return [type.id.join('::'), convert(fund, type)]; + }); + + return { + definitions: collect(schemas), + }; +} diff --git a/src/index.js b/src/index.js index 027f58a..ec97992 100644 --- a/src/index.js +++ b/src/index.js @@ -1,11 +1,13 @@ import Parser from './parser'; import Collector from './collector'; import type {Type} from './types'; +import generateJsonSchema from './generators/jsonSchema'; +import type {Schema} from './generators/jsonSchema'; // @see babel#6805. //export {Parser, Collector}; -export default function (path: string): {+types: Type[]} { +export default function (path: string): {+types: Type[], +schema: Schema} { const parser = new Parser; const collector = new Collector(parser); @@ -14,6 +16,7 @@ export default function (path: string): {+types: Type[]} { const fund = collector.finish(); return { - types: fund.flatten(), + types: Array.from(fund.takeAll()), + schema: generateJsonSchema(fund), }; } diff --git a/src/utils.js b/src/utils.js index 0e528b2..2de1caf 100644 --- a/src/utils.js +++ b/src/utils.js @@ -9,3 +9,13 @@ export function last(list: T[]): T { return list[list.length - 1]; } + +export function collect(iter: Iterable<[string, T]>): {[string]: T} { + const result = {}; + + for (const [key, value] of iter) { + result[key] = value; + } + + return result; +}