flow2schema/src/generators/jsonSchema.js

205 lines
5.8 KiB
JavaScript

// @flow
import wu from 'wu';
import {invariant, collect, partition} 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 = boolean | {
id?: string,
$ref?: string,
$schema?: string,
$comment?: 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 {
let schema = convertType(fund, type);
if (type && type.comment) {
if (schema === true) {
schema = { $comment: type.comment };
} else {
schema.$comment = type.comment;
}
}
return schema;
}
function convertType(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();
return required.length > 0 ? {
type: 'object',
properties,
required,
} : {
type: 'object',
properties,
};
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');
// TODO: "propertyNames".
return {
type: 'object',
additionalProperties: convert(fund, type.values),
};
case 'union':
const enumerate = wu(type.variants)
.filter(variant => variant.kind === 'literal' && variant.value !== null)
.map(literal => (literal: $FlowFixMe).value)
.tap(value => invariant(value !== undefined))
.toArray();
const schemas = wu(type.variants)
.filter(variant => variant.kind !== 'literal' || variant.value === null)
.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':
const [maps, others] = partition(type.parts, type => type.kind === 'map');
const parts = wu(others).map(part => convert(fund, part)).toArray();
if (maps.length > 0) {
const keys = wu(maps).map(map => convert(fund, (map: $FlowFixMe).values)).toArray();
const key = keys.length === 1 ? keys[0] : {anyOf: keys};
if (parts.length === 1 && parts[0].type === 'object') {
invariant(typeof parts[0] === 'object');
invariant(parts[0].additionalProperties == null);
parts[0].additionalProperties = key;
} else {
parts.push({
type: 'object',
additionalProperties: key,
});
}
}
return parts.length === 1 ? parts[0] : {
allOf: parts,
};
case 'maybe':
return {
anyOf: [convert(fund, type.value), {type: 'null'}],
};
case 'number':
const {repr} = type;
return repr === 'f32' || repr === 'f64' ? {type: 'number'}
: repr === 'i32' || repr === 'i64' ? {type: 'integer'}
: {type: 'integer', minimum: 0};
case 'string':
return {
type: 'string',
};
case 'boolean':
return {
type: 'boolean',
};
case 'literal':
invariant(type.value !== undefined);
return type.value === null ? {
type: 'null',
} : {
const: type.value,
};
case 'any':
case 'mixed':
return true;
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 {
$schema: 'http://json-schema.org/draft-06/schema#',
definitions: collect(schemas),
};
}