205 lines
5.8 KiB
JavaScript
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),
|
|
};
|
|
}
|