Merge pull request #654 from quicktype/foreign-schema-refs
Foreign schema refs
This commit is contained in:
Коммит
946467a718
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE
|
|
@ -0,0 +1,16 @@
|
|||
# Installation
|
||||
> `npm install --save @types/urijs`
|
||||
|
||||
# Summary
|
||||
This package contains type definitions for URI.js (https://github.com/medialize/URI.js).
|
||||
|
||||
# Details
|
||||
Files were exported from https://www.github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/urijs
|
||||
|
||||
Additional Details
|
||||
* Last updated: Fri, 16 Feb 2018 00:16:49 GMT
|
||||
* Dependencies: jquery
|
||||
* Global values: URI, URITemplate
|
||||
|
||||
# Credits
|
||||
These definitions were written by RodneyJT <https://github.com/RodneyJT>, Brian Surowiec <https://github.com/xt0rted>, Pete Johanson <https://github.com/petejohanson>.
|
|
@ -0,0 +1,263 @@
|
|||
// Type definitions for URI.js 1.15.1
|
||||
// Project: https://github.com/medialize/URI.js
|
||||
// Definitions by: RodneyJT <https://github.com/RodneyJT>, Brian Surowiec <https://github.com/xt0rted>, Pete Johanson <https://github.com/petejohanson>
|
||||
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
|
||||
// TypeScript Version: 2.3
|
||||
|
||||
declare namespace uri {
|
||||
|
||||
interface URI {
|
||||
absoluteTo(path: string): URI;
|
||||
absoluteTo(path: URI): URI;
|
||||
addFragment(fragment: string): URI;
|
||||
addQuery(qry: string): URI;
|
||||
addQuery(qry: Object): URI;
|
||||
addSearch(qry: string): URI;
|
||||
addSearch(key: string, value:any): URI;
|
||||
addSearch(qry: Object): URI;
|
||||
authority(): string;
|
||||
authority(authority: string): URI;
|
||||
|
||||
clone(): URI;
|
||||
|
||||
directory(): string;
|
||||
directory(dir: boolean): string;
|
||||
directory(dir: string): URI;
|
||||
domain(): string;
|
||||
domain(domain: boolean): string;
|
||||
domain(domain: string): URI;
|
||||
|
||||
duplicateQueryParameters(val: boolean): URI;
|
||||
|
||||
equals(): boolean;
|
||||
equals(url: string | URI): boolean;
|
||||
|
||||
filename(): string;
|
||||
filename(file: boolean): string;
|
||||
filename(file: string): URI;
|
||||
fragment(): string;
|
||||
fragment(fragment: string): URI;
|
||||
fragmentPrefix(prefix: string): URI;
|
||||
|
||||
hash(): string;
|
||||
hash(hash: string): URI;
|
||||
host(): string;
|
||||
host(host: string): URI;
|
||||
hostname(): string;
|
||||
hostname(hostname: string): URI;
|
||||
href(): string;
|
||||
href(url: string): void;
|
||||
|
||||
is(qry: string): boolean;
|
||||
iso8859(): URI;
|
||||
|
||||
normalize(): URI;
|
||||
normalizeFragment(): URI;
|
||||
normalizeHash(): URI;
|
||||
normalizeHostname(): URI;
|
||||
normalizePath(): URI;
|
||||
normalizePathname(): URI;
|
||||
normalizePort(): URI;
|
||||
normalizeProtocol(): URI;
|
||||
normalizeQuery(): URI;
|
||||
normalizeSearch(): URI;
|
||||
|
||||
origin(): string;
|
||||
origin(uri: string | URI): URI;
|
||||
|
||||
password(): string;
|
||||
password(pw: string): URI;
|
||||
path(): string;
|
||||
path(path: boolean): string;
|
||||
path(path: string): URI;
|
||||
pathname(): string;
|
||||
pathname(path: boolean): string;
|
||||
pathname(path: string): URI;
|
||||
port(): string;
|
||||
port(port: string): URI;
|
||||
protocol(): string;
|
||||
protocol(protocol: string): URI;
|
||||
|
||||
query(): string;
|
||||
query(qry: string): URI;
|
||||
query(qry: boolean): Object;
|
||||
query(qry: Object): URI;
|
||||
|
||||
readable(): string;
|
||||
relativeTo(path: string): URI;
|
||||
removeQuery(qry: string): URI;
|
||||
removeQuery(qry: Object): URI;
|
||||
removeQuery(name: string, value: string): URI;
|
||||
removeSearch(qry: string): URI;
|
||||
removeSearch(qry: Object): URI;
|
||||
removeSearch(name: string, value: string): URI;
|
||||
resource(): string;
|
||||
resource(resource: string): URI;
|
||||
|
||||
scheme(): string;
|
||||
scheme(protocol: string): URI;
|
||||
search(): string;
|
||||
search(qry: string): URI;
|
||||
search(qry: boolean): any;
|
||||
search(qry: Object): URI;
|
||||
segment(): string[];
|
||||
segment(segments: string[]): URI;
|
||||
segment(position: number): string;
|
||||
segment(position: number, level: string): URI;
|
||||
segment(segment: string): URI;
|
||||
segmentCoded(): string[];
|
||||
segmentCoded(segments: string[]): URI;
|
||||
segmentCoded(position: number): string;
|
||||
segmentCoded(position: number, level: string): URI;
|
||||
segmentCoded(segment: string): URI;
|
||||
setQuery(key: string, value: string): URI;
|
||||
setQuery(qry: Object): URI;
|
||||
setSearch(key: string, value: string): URI;
|
||||
setSearch(qry: Object): URI;
|
||||
hasQuery(name: string | any, value?: string | number | boolean | Function | Array<string> | Array<number> | Array<boolean> | RegExp, withinArray?: boolean): boolean;
|
||||
hasSearch(name: string | any, value?: string | number | boolean | Function | Array<string> | Array<number> | Array<boolean> | RegExp, withinArray?: boolean): boolean;
|
||||
subdomain(): string;
|
||||
subdomain(subdomain: string): URI;
|
||||
suffix(): string;
|
||||
suffix(suffix: boolean): string;
|
||||
suffix(suffix: string): URI;
|
||||
|
||||
tld(): string;
|
||||
tld(tld: boolean): string;
|
||||
tld(tld: string): URI;
|
||||
|
||||
unicode(): URI;
|
||||
userinfo(): string;
|
||||
userinfo(userinfo: string): URI;
|
||||
username(): string;
|
||||
username(uname: string): URI;
|
||||
|
||||
valueOf(): string;
|
||||
}
|
||||
|
||||
interface URIOptions {
|
||||
protocol?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
hostname?: string;
|
||||
port?: string;
|
||||
path?: string;
|
||||
query?: string;
|
||||
fragment?: string;
|
||||
}
|
||||
|
||||
interface URIStatic {
|
||||
(): URI;
|
||||
(value: string | URIOptions): URI;
|
||||
|
||||
new (): URI;
|
||||
new (value: string | URIOptions): URI;
|
||||
|
||||
addQuery(data: Object, prop: string, value: string): Object;
|
||||
addQuery(data: Object, qryObj: Object): Object;
|
||||
|
||||
build(parts: {
|
||||
protocol: string;
|
||||
username: string;
|
||||
password: string;
|
||||
hostname: string;
|
||||
port: string;
|
||||
path: string;
|
||||
query: string;
|
||||
fragment: string;
|
||||
}): string;
|
||||
buildAuthority(parts: {
|
||||
username?: string;
|
||||
password?: string;
|
||||
hostname?: string;
|
||||
port?: string;
|
||||
}): string;
|
||||
buildHost(parts: {
|
||||
hostname?: string;
|
||||
port?: string;
|
||||
}): string;
|
||||
buildQuery(qry: Object): string;
|
||||
buildQuery(qry: Object, duplicates: boolean): string;
|
||||
buildUserinfo(parts: {
|
||||
username?: string;
|
||||
password?: string;
|
||||
}): string;
|
||||
|
||||
commonPath(path1: string, path2: string): string;
|
||||
|
||||
decode(str: string): string;
|
||||
decodeQuery(qry: string): string;
|
||||
|
||||
encode(str: string): string;
|
||||
encodeQuery(qry: string): string;
|
||||
encodeReserved(str: string): string;
|
||||
expand(template: string, vals: Object): URI;
|
||||
|
||||
iso8859(): void;
|
||||
|
||||
joinPaths(...paths: (string | URI)[]): URI;
|
||||
|
||||
parse(url: string): {
|
||||
protocol: string;
|
||||
username: string;
|
||||
password: string;
|
||||
hostname: string;
|
||||
port: string;
|
||||
path: string;
|
||||
query: string;
|
||||
fragment: string;
|
||||
};
|
||||
parseAuthority(url: string, parts: {
|
||||
username?: string;
|
||||
password?: string;
|
||||
hostname?: string;
|
||||
port?: string;
|
||||
}): string;
|
||||
parseHost(url: string, parts: {
|
||||
hostname?: string;
|
||||
port?: string;
|
||||
}): string;
|
||||
parseQuery(url: string): Object;
|
||||
parseUserinfo(url: string, parts: {
|
||||
username?: string;
|
||||
password?: string;
|
||||
}): string;
|
||||
|
||||
removeQuery(data: Object, prop: string, value: string): Object;
|
||||
removeQuery(data: Object, props: string[]): Object;
|
||||
removeQuery(data: Object, props: Object): Object;
|
||||
|
||||
unicode(): void;
|
||||
|
||||
withinString(source: string, func: (url: string) => string): string;
|
||||
}
|
||||
|
||||
type URITemplateValue = string | ReadonlyArray<string> | { [key: string] : string } | undefined | null;
|
||||
type URITemplateCallback = (keyName: string) => URITemplateValue;
|
||||
type URITemplateInput = { [key: string]: URITemplateValue | URITemplateCallback } | URITemplateCallback;
|
||||
|
||||
interface URITemplate {
|
||||
expand(data: URITemplateInput, opts?: Object) : URI;
|
||||
}
|
||||
|
||||
interface URITemplateStatic {
|
||||
(template: string) : URITemplate;
|
||||
|
||||
new (template: string) : URITemplate;
|
||||
}
|
||||
}
|
||||
|
||||
declare var URI: uri.URIStatic;
|
||||
declare var URITemplate : uri.URITemplateStatic;
|
||||
|
||||
declare module 'URI' {
|
||||
export = URI;
|
||||
}
|
||||
|
||||
declare module 'urijs' {
|
||||
export = URI;
|
||||
}
|
||||
|
||||
declare module 'urijs/src/URITemplate' {
|
||||
export = URITemplate;
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"_from": "@types/urijs",
|
||||
"_id": "@types/urijs@1.15.36",
|
||||
"_inBundle": false,
|
||||
"_integrity":
|
||||
"sha512-VTtBKU7xugrSODKR/lpO2eohFiZZ60BsiPlS1Lf9MHsBjhF6FeR2Uc0/Qtlb151Wz9u0+BLVjx/xbRIsMCTb3Q==",
|
||||
"_location": "/@types/urijs",
|
||||
"_phantomChildren": {},
|
||||
"_requested": {
|
||||
"type": "tag",
|
||||
"registry": true,
|
||||
"raw": "@types/urijs",
|
||||
"name": "@types/urijs",
|
||||
"escapedName": "@types%2furijs",
|
||||
"scope": "@types",
|
||||
"rawSpec": "",
|
||||
"saveSpec": null,
|
||||
"fetchSpec": "latest"
|
||||
},
|
||||
"_requiredBy": ["#DEV:/", "#USER"],
|
||||
"_resolved": "https://registry.npmjs.org/@types/urijs/-/urijs-1.15.36.tgz",
|
||||
"_shasum": "13e481d2fedad94880080a32b739388bc3b52e4b",
|
||||
"_spec": "@types/urijs",
|
||||
"_where": "/Users/schani/Work/quicktype",
|
||||
"bundleDependencies": false,
|
||||
"contributors": [
|
||||
{
|
||||
"name": "RodneyJT",
|
||||
"url": "https://github.com/RodneyJT"
|
||||
},
|
||||
{
|
||||
"name": "Brian Surowiec",
|
||||
"url": "https://github.com/xt0rted"
|
||||
},
|
||||
{
|
||||
"name": "Pete Johanson",
|
||||
"url": "https://github.com/petejohanson"
|
||||
}
|
||||
],
|
||||
"deprecated": false,
|
||||
"description": "TypeScript definitions for URI.js",
|
||||
"license": "MIT",
|
||||
"main": "",
|
||||
"name": "@types/urijs",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://www.github.com/DefinitelyTyped/DefinitelyTyped.git"
|
||||
},
|
||||
"scripts": {},
|
||||
"typeScriptVersion": "2.3",
|
||||
"typesPublisherContentHash":
|
||||
"35f15ad31c8beb76fd8929afad2b894e7bfd4640b88d2944a3e0a9c0a79e29da",
|
||||
"version": "1.15.36"
|
||||
}
|
|
@ -26,12 +26,13 @@
|
|||
"lodash": "^4.17.4",
|
||||
"moment": "^2.19.3",
|
||||
"node-fetch": "^1.7.1",
|
||||
"pkg": "^4.3.0",
|
||||
"pako": "^1.0.6",
|
||||
"pkg": "^4.3.0",
|
||||
"pluralize": "^7.0.0",
|
||||
"stream-json": "0.5.2",
|
||||
"string-to-stream": "^1.1.0",
|
||||
"unicode-properties": "quicktype/unicode-properties#dist"
|
||||
"unicode-properties": "quicktype/unicode-properties#dist",
|
||||
"urijs": "^1.19.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/graphql": "^0.11.7",
|
||||
|
@ -43,6 +44,7 @@
|
|||
"@types/pako": "^1.0.0",
|
||||
"@types/pluralize": "0.0.28",
|
||||
"@types/shelljs": "^0.7.8",
|
||||
"@types/urijs": "file:lib/@types/urijs",
|
||||
"ajv": "^5.2.2",
|
||||
"compare-versions": "^3.1.0",
|
||||
"deep-equal": "^1.0.1",
|
||||
|
|
|
@ -149,5 +149,5 @@ export function combineClasses(
|
|||
);
|
||||
}
|
||||
|
||||
return graph.rewrite(stringTypeMapping, alphabetizeProperties, cliques, makeCliqueClass);
|
||||
return graph.rewrite("combine classes", stringTypeMapping, alphabetizeProperties, cliques, makeCliqueClass);
|
||||
}
|
||||
|
|
|
@ -41,10 +41,10 @@ function splitDescription(descriptions: OrderedSet<string> | undefined): string[
|
|||
|
||||
export type ForbiddenWordsInfo = { names: (Name | string)[]; includeGlobalForbidden: boolean };
|
||||
|
||||
const assignedNameAttributeKind = new TypeAttributeKind<Name>("assignedName", undefined, undefined);
|
||||
const assignedPropertyNamesAttributeKind = new TypeAttributeKind<Map<string, Name>>("assignedPropertyNames", undefined, undefined);
|
||||
const assignedMemberNamesAttributeKind = new TypeAttributeKind<Map<Type, Name>>("assignedMemberNames", undefined, undefined);
|
||||
const assignedCaseNamesAttributeKind = new TypeAttributeKind<Map<string, Name>>("assignedCaseNames", undefined, undefined);
|
||||
const assignedNameAttributeKind = new TypeAttributeKind<Name>("assignedName", undefined, undefined, undefined);
|
||||
const assignedPropertyNamesAttributeKind = new TypeAttributeKind<Map<string, Name>>("assignedPropertyNames", undefined, undefined, undefined);
|
||||
const assignedMemberNamesAttributeKind = new TypeAttributeKind<Map<Type, Name>>("assignedMemberNames", undefined, undefined, undefined);
|
||||
const assignedCaseNamesAttributeKind = new TypeAttributeKind<Map<string, Name>>("assignedCaseNames", undefined, undefined, undefined);
|
||||
|
||||
export abstract class ConvenienceRenderer extends Renderer {
|
||||
private _globalForbiddenNamespace: Namespace | undefined;
|
||||
|
|
|
@ -75,7 +75,7 @@ export function flattenUnions(
|
|||
}
|
||||
});
|
||||
singleTypeGroups.forEach(ts => groups.push(ts.toArray()));
|
||||
graph = graph.rewrite(stringTypeMapping, false, groups, replace);
|
||||
graph = graph.rewrite("flatten unions", stringTypeMapping, false, groups, replace);
|
||||
|
||||
// console.log(`flattened ${nonCanonicalUnions.size} of ${unions.size} unions`);
|
||||
return [graph, !needsRepeat && !foundIntersection];
|
||||
|
|
|
@ -59,7 +59,7 @@ function replaceUnion(group: Set<UnionType>, builder: GraphRewriteBuilder<UnionT
|
|||
});
|
||||
if (types.length === 0) {
|
||||
return builder.getStringType(
|
||||
combineTypeAttributes([stringAttributes, u.getAttributes()]),
|
||||
combineTypeAttributes(stringAttributes, u.getAttributes()),
|
||||
undefined,
|
||||
forwardingRef
|
||||
);
|
||||
|
@ -74,7 +74,7 @@ export function inferEnums(graph: TypeGraph, stringTypeMapping: StringTypeMappin
|
|||
.filter(t => t instanceof StringType)
|
||||
.map(t => [t])
|
||||
.toArray() as StringType[][];
|
||||
return graph.rewrite(stringTypeMapping, false, allStrings, replaceString);
|
||||
return graph.rewrite("infer enums", stringTypeMapping, false, allStrings, replaceString);
|
||||
}
|
||||
|
||||
export function flattenStrings(graph: TypeGraph, stringTypeMapping: StringTypeMapping): TypeGraph {
|
||||
|
@ -83,5 +83,5 @@ export function flattenStrings(graph: TypeGraph, stringTypeMapping: StringTypeMa
|
|||
.filter(unionNeedsReplacing)
|
||||
.map(t => [t])
|
||||
.toArray();
|
||||
return graph.rewrite(stringTypeMapping, false, unionsToReplace, replaceUnion);
|
||||
return graph.rewrite("flatten strings", stringTypeMapping, false, unionsToReplace, replaceUnion);
|
||||
}
|
||||
|
|
|
@ -120,5 +120,5 @@ export function inferMaps(graph: TypeGraph, stringTypeMapping: StringTypeMapping
|
|||
|
||||
const allClasses = graph.allNamedTypesSeparated().classes;
|
||||
const classesToReplace = allClasses.filter(c => !c.isFixed && shouldBeMap(c.properties) !== undefined).toArray();
|
||||
return graph.rewrite(stringTypeMapping, false, classesToReplace.map(c => [c]), replaceClass);
|
||||
return graph.rewrite("infer maps", stringTypeMapping, false, classesToReplace.map(c => [c]), replaceClass);
|
||||
}
|
||||
|
|
|
@ -2,9 +2,20 @@
|
|||
|
||||
import { List, OrderedSet, Map, Set, hash } from "immutable";
|
||||
import * as pluralize from "pluralize";
|
||||
import * as URI from "urijs";
|
||||
|
||||
import { ClassProperty } from "./Type";
|
||||
import { panic, assertNever, StringMap, checkStringMap, assert, defined, addHashCode, hashCodeInit } from "./Support";
|
||||
import { ClassProperty, PrimitiveTypeKind } from "./Type";
|
||||
import {
|
||||
panic,
|
||||
assertNever,
|
||||
StringMap,
|
||||
checkStringMap,
|
||||
assert,
|
||||
defined,
|
||||
addHashCode,
|
||||
mapSync,
|
||||
forEachSync
|
||||
} from "./Support";
|
||||
import { TypeGraphBuilder, TypeRef } from "./TypeBuilder";
|
||||
import { TypeNames } from "./TypeNames";
|
||||
import { makeNamesTypeAttributes, modifyTypeNames, singularizeTypeNames } from "./TypeNames";
|
||||
|
@ -15,13 +26,15 @@ import {
|
|||
makeTypeAttributesInferred
|
||||
} from "./TypeAttributes";
|
||||
|
||||
enum PathElementKind {
|
||||
export enum PathElementKind {
|
||||
Root,
|
||||
KeyOrIndex,
|
||||
Type,
|
||||
Object
|
||||
}
|
||||
|
||||
type PathElement =
|
||||
export type PathElement =
|
||||
| { kind: PathElementKind.Root }
|
||||
| { kind: PathElementKind.KeyOrIndex; key: string }
|
||||
| { kind: PathElementKind.Type; index: number }
|
||||
| { kind: PathElementKind.Object };
|
||||
|
@ -43,33 +56,74 @@ function pathElementEquals(a: PathElement, b: PathElement): boolean {
|
|||
}
|
||||
}
|
||||
|
||||
export type JSONSchema = StringMap | boolean;
|
||||
|
||||
export function checkJSONSchema(x: any): JSONSchema {
|
||||
if (typeof x === "boolean") return x;
|
||||
if (Array.isArray(x)) return panic("An array is not a valid JSON Schema");
|
||||
if (x === null) return panic("null is not a valid JSON Schema");
|
||||
if (typeof x !== "object") return panic("Only booleans and objects can be valid JSON Schemas");
|
||||
return x;
|
||||
}
|
||||
|
||||
const numberRegexp = new RegExp("^[0-9]+$");
|
||||
|
||||
export class Ref {
|
||||
static readonly root: Ref = new Ref(List());
|
||||
static root(address: string): Ref {
|
||||
const uri = new URI(address);
|
||||
return new Ref(uri, List());
|
||||
}
|
||||
|
||||
private static parsePath(path: string): List<PathElement> {
|
||||
const elements: PathElement[] = [];
|
||||
|
||||
if (path.startsWith("/")) {
|
||||
elements.push({ kind: PathElementKind.Root });
|
||||
path = path.substr(1);
|
||||
}
|
||||
|
||||
if (path !== "") {
|
||||
const parts = path.split("/");
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
elements.push({ kind: PathElementKind.KeyOrIndex, key: parts[i] });
|
||||
}
|
||||
}
|
||||
return List(elements);
|
||||
}
|
||||
|
||||
static parse(ref: any): Ref {
|
||||
if (typeof ref !== "string") {
|
||||
return panic("$ref must be a string");
|
||||
}
|
||||
if (!ref.startsWith("#/")) {
|
||||
return panic('$ref must start with "#/"');
|
||||
}
|
||||
ref = ref.substr(2);
|
||||
if (ref === "") return Ref.root;
|
||||
|
||||
const elements: PathElement[] = [];
|
||||
const parts = ref.split("/");
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
elements.push({ kind: PathElementKind.KeyOrIndex, key: parts[i] });
|
||||
}
|
||||
return new Ref(List(elements));
|
||||
const uri = new URI(ref);
|
||||
const path = uri.fragment();
|
||||
uri.fragment("");
|
||||
const elements = Ref.parsePath(path);
|
||||
return new Ref(uri, elements);
|
||||
}
|
||||
|
||||
private constructor(private readonly _path: List<PathElement>) { }
|
||||
public addressURI: uri.URI | undefined;
|
||||
|
||||
constructor(addressURI: uri.URI | undefined, readonly path: List<PathElement>) {
|
||||
if (addressURI !== undefined) {
|
||||
assert(addressURI.fragment() === "", `Ref URI with fragment is not allowed: ${addressURI.toString()}`);
|
||||
this.addressURI = addressURI.clone().normalize();
|
||||
} else {
|
||||
this.addressURI = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
get hasAddress(): boolean {
|
||||
return this.addressURI !== undefined;
|
||||
}
|
||||
|
||||
get address(): string {
|
||||
return defined(this.addressURI).toString();
|
||||
}
|
||||
|
||||
private pushElement(pe: PathElement): Ref {
|
||||
return new Ref(this._path.push(pe));
|
||||
return new Ref(this.addressURI, this.path.push(pe));
|
||||
}
|
||||
|
||||
push(...keys: string[]): Ref {
|
||||
|
@ -88,13 +142,29 @@ export class Ref {
|
|||
return this.pushElement({ kind: PathElementKind.Type, index });
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
let path = this._path;
|
||||
resolveAgainst(base: Ref | undefined): Ref {
|
||||
let addressURI = this.addressURI;
|
||||
if (base !== undefined && base.addressURI !== undefined) {
|
||||
addressURI = addressURI === undefined ? base.addressURI : addressURI.absoluteTo(base.addressURI);
|
||||
}
|
||||
return new Ref(addressURI, this.path);
|
||||
}
|
||||
|
||||
for (; ;) {
|
||||
get name(): string {
|
||||
let path = this.path;
|
||||
|
||||
for (;;) {
|
||||
const e = path.last();
|
||||
if (e === undefined) {
|
||||
return "Something";
|
||||
if (e === undefined || e.kind === PathElementKind.Root) {
|
||||
let name = this.addressURI !== undefined ? this.addressURI.filename() : "";
|
||||
const suffix = this.addressURI !== undefined ? this.addressURI.suffix() : "";
|
||||
if (name.length > suffix.length + 1) {
|
||||
name = name.substr(0, name.length - suffix.length - 1);
|
||||
}
|
||||
if (name === "") {
|
||||
return "Something";
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
switch (e.kind) {
|
||||
|
@ -115,15 +185,17 @@ export class Ref {
|
|||
}
|
||||
|
||||
get definitionName(): string | undefined {
|
||||
const pe = this._path.get(-2);
|
||||
const pe = this.path.get(-2);
|
||||
if (pe === undefined) return undefined;
|
||||
if (keyOrIndex(pe) === "definitions") return keyOrIndex(defined(this._path.last()));
|
||||
if (keyOrIndex(pe) === "definitions") return keyOrIndex(defined(this.path.last()));
|
||||
return undefined;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
function elementToString(e: PathElement): string {
|
||||
switch (e.kind) {
|
||||
case PathElementKind.Root:
|
||||
return "/";
|
||||
case PathElementKind.Type:
|
||||
return `type/${e.index.toString()}`;
|
||||
case PathElementKind.Object:
|
||||
|
@ -134,20 +206,20 @@ export class Ref {
|
|||
return assertNever(e);
|
||||
}
|
||||
}
|
||||
return "#/" + this._path.map(elementToString).join("/");
|
||||
const address = this.addressURI === undefined ? "" : this.addressURI.toString();
|
||||
return address + "#" + this.path.map(elementToString).join("/");
|
||||
}
|
||||
|
||||
lookupRef(root: StringMap): StringMap {
|
||||
function lookup(
|
||||
local: StringMap | any[],
|
||||
path: List<PathElement>
|
||||
): StringMap {
|
||||
lookupRef(root: JSONSchema): JSONSchema {
|
||||
function lookup(local: any, path: List<PathElement>): JSONSchema {
|
||||
const first = path.first();
|
||||
if (first === undefined) {
|
||||
return checkStringMap(local);
|
||||
return checkJSONSchema(local);
|
||||
}
|
||||
const rest = path.rest();
|
||||
switch (first.kind) {
|
||||
case PathElementKind.Root:
|
||||
return lookup(root, rest);
|
||||
case PathElementKind.KeyOrIndex:
|
||||
if (Array.isArray(local)) {
|
||||
return lookup(local[parseInt(first.key, 10)], rest);
|
||||
|
@ -162,18 +234,23 @@ export class Ref {
|
|||
return assertNever(first);
|
||||
}
|
||||
}
|
||||
return lookup(root, this._path);
|
||||
return lookup(root, this.path);
|
||||
}
|
||||
|
||||
equals(other: any): boolean {
|
||||
if (!(other instanceof Ref)) return false;
|
||||
if (this._path.size !== other._path.size) return false;
|
||||
return this._path.zipWith(pathElementEquals, other._path).every(x => x);
|
||||
if (this.addressURI !== undefined && other.addressURI !== undefined) {
|
||||
if (!this.addressURI.equals(other.addressURI)) return false;
|
||||
} else {
|
||||
if ((this.addressURI === undefined) !== (other.addressURI === undefined)) return false;
|
||||
}
|
||||
if (this.path.size !== other.path.size) return false;
|
||||
return this.path.zipWith(pathElementEquals, other.path).every(x => x);
|
||||
}
|
||||
|
||||
hashCode(): number {
|
||||
let acc = hashCodeInit;
|
||||
this._path.forEach(pe => {
|
||||
let acc = hash(this.addressURI !== undefined ? this.addressURI.toString() : undefined);
|
||||
this.path.forEach(pe => {
|
||||
acc = addHashCode(acc, pe.kind);
|
||||
switch (pe.kind) {
|
||||
case PathElementKind.Type:
|
||||
|
@ -182,12 +259,132 @@ export class Ref {
|
|||
case PathElementKind.KeyOrIndex:
|
||||
acc = addHashCode(acc, hash(pe.key));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
return acc;
|
||||
}
|
||||
}
|
||||
|
||||
class Location {
|
||||
public readonly canonicalRef: Ref;
|
||||
public readonly virtualRef: Ref;
|
||||
|
||||
constructor(canonicalRef: Ref, virtualRef?: Ref) {
|
||||
this.canonicalRef = canonicalRef;
|
||||
this.virtualRef = virtualRef !== undefined ? virtualRef : canonicalRef;
|
||||
}
|
||||
|
||||
updateWithID(id: any) {
|
||||
if (typeof id !== "string") return this;
|
||||
// FIXME: This is incorrect. If the parsed ref doesn't have an address, the
|
||||
// current virtual one's must be used. The canonizer must do this, too.
|
||||
return new Location(this.canonicalRef, Ref.parse(id).resolveAgainst(this.virtualRef));
|
||||
}
|
||||
|
||||
push(...keys: string[]): Location {
|
||||
return new Location(this.canonicalRef.push(...keys), this.virtualRef.push(...keys));
|
||||
}
|
||||
|
||||
pushObject(): Location {
|
||||
return new Location(this.canonicalRef.pushObject(), this.virtualRef.pushObject());
|
||||
}
|
||||
|
||||
pushType(index: number): Location {
|
||||
return new Location(this.canonicalRef.pushType(index), this.virtualRef.pushType(index));
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `${this.virtualRef.toString()} (${this.canonicalRef.toString()})`;
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class JSONSchemaStore {
|
||||
private _schemas: Map<string, JSONSchema> = Map();
|
||||
|
||||
private add(address: string, schema: JSONSchema): void {
|
||||
assert(!this._schemas.has(address), "Cannot set a schema for an address twice");
|
||||
this._schemas = this._schemas.set(address, schema);
|
||||
}
|
||||
|
||||
abstract async fetch(_address: string): Promise<JSONSchema | undefined>;
|
||||
|
||||
async get(address: string): Promise<JSONSchema> {
|
||||
let schema = this._schemas.get(address);
|
||||
if (schema !== undefined) {
|
||||
return schema;
|
||||
}
|
||||
schema = await this.fetch(address);
|
||||
if (schema === undefined) {
|
||||
return panic(`Schema at address "${address}" not available`);
|
||||
}
|
||||
this.add(address, schema);
|
||||
return schema;
|
||||
}
|
||||
}
|
||||
|
||||
class Canonizer {
|
||||
private _map: Map<Ref, Ref> = Map();
|
||||
private _schemaAddressesAdded: Set<string> = Set();
|
||||
|
||||
private addID(mapped: string, loc: Location): void {
|
||||
const ref = Ref.parse(mapped).resolveAgainst(loc.virtualRef);
|
||||
assert(ref.hasAddress, "$id must have an address");
|
||||
this._map = this._map.set(ref, loc.canonicalRef);
|
||||
}
|
||||
|
||||
private addIDs(schema: any, loc: Location) {
|
||||
if (schema === null) return;
|
||||
if (Array.isArray(schema)) {
|
||||
for (let i = 0; i < schema.length; i++) {
|
||||
this.addIDs(schema[i], loc.push(i.toString()));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (typeof schema !== "object") {
|
||||
return;
|
||||
}
|
||||
const maybeID = schema["$id"];
|
||||
if (typeof maybeID === "string") {
|
||||
this.addID(maybeID, loc);
|
||||
loc = loc.updateWithID(maybeID);
|
||||
}
|
||||
for (const property of Object.getOwnPropertyNames(schema)) {
|
||||
this.addIDs(schema[property], loc.push(property));
|
||||
}
|
||||
}
|
||||
|
||||
addSchema(schema: any, address: string) {
|
||||
if (this._schemaAddressesAdded.has(address)) return;
|
||||
|
||||
this.addIDs(schema, new Location(Ref.root(address)));
|
||||
this._schemaAddressesAdded = this._schemaAddressesAdded.add(address);
|
||||
}
|
||||
|
||||
// Returns: Canonical ref, full virtual ref
|
||||
canonize(virtualBase: Ref | undefined, ref: Ref): [Ref, Ref] {
|
||||
const fullVirtual = ref.resolveAgainst(virtualBase);
|
||||
let virtual = fullVirtual;
|
||||
let relative: List<PathElement> = List();
|
||||
for (;;) {
|
||||
const maybeCanonical = this._map.get(virtual);
|
||||
if (maybeCanonical !== undefined) {
|
||||
return [new Ref(maybeCanonical.addressURI, maybeCanonical.path.concat(relative)), fullVirtual];
|
||||
}
|
||||
const last = virtual.path.last();
|
||||
if (last === undefined) {
|
||||
// We've exhausted our options - it's not a mapped ref.
|
||||
return [fullVirtual, fullVirtual];
|
||||
}
|
||||
if (last.kind !== PathElementKind.Root) {
|
||||
relative = relative.unshift(last);
|
||||
}
|
||||
virtual = new Ref(virtual.addressURI, virtual.path.pop());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkStringArray(arr: any): string[] {
|
||||
if (!Array.isArray(arr)) {
|
||||
return panic(`Expected a string array, but got ${arr}`);
|
||||
|
@ -200,7 +397,7 @@ function checkStringArray(arr: any): string[] {
|
|||
return arr;
|
||||
}
|
||||
|
||||
function makeAttributes(schema: StringMap, path: Ref, attributes: TypeAttributes): TypeAttributes {
|
||||
function makeAttributes(schema: StringMap, loc: Location, attributes: TypeAttributes): TypeAttributes {
|
||||
const maybeDescription = schema.description;
|
||||
if (typeof maybeDescription === "string") {
|
||||
attributes = descriptionTypeAttributeKind.setInAttributes(attributes, OrderedSet([maybeDescription]));
|
||||
|
@ -212,7 +409,7 @@ function makeAttributes(schema: StringMap, path: Ref, attributes: TypeAttributes
|
|||
}
|
||||
let title = schema.title;
|
||||
if (typeof title !== "string") {
|
||||
title = path.definitionName;
|
||||
title = loc.canonicalRef.definitionName;
|
||||
}
|
||||
|
||||
if (typeof title === "string") {
|
||||
|
@ -242,20 +439,42 @@ function checkTypeList(typeOrTypes: any): OrderedSet<string> {
|
|||
}
|
||||
}
|
||||
|
||||
export function addTypesInSchema(typeBuilder: TypeGraphBuilder, rootJson: any, references: Map<string, Ref>): void {
|
||||
const root = checkStringMap(rootJson);
|
||||
let typeForPath = Map<Ref, TypeRef>();
|
||||
export async function addTypesInSchema(
|
||||
typeBuilder: TypeGraphBuilder,
|
||||
store: JSONSchemaStore,
|
||||
references: Map<string, Ref>
|
||||
): Promise<void> {
|
||||
const canonizer = new Canonizer();
|
||||
|
||||
function setTypeForPath(path: Ref, t: TypeRef): void {
|
||||
const maybeRef = typeForPath.get(path);
|
||||
async function resolveVirtualRef(base: Location | undefined, virtualRef: Ref): Promise<[JSONSchema, Location]> {
|
||||
const [canonical, fullVirtual] = canonizer.canonize(
|
||||
base !== undefined ? base.virtualRef : undefined,
|
||||
virtualRef
|
||||
);
|
||||
assert(canonical.hasAddress, "Canonical ref can't be resolved without an address");
|
||||
const schema = await store.get(canonical.address);
|
||||
canonizer.addSchema(schema, canonical.address);
|
||||
return [canonical.lookupRef(schema), new Location(canonical, fullVirtual)];
|
||||
}
|
||||
|
||||
let typeForCanonicalRef = Map<Ref, TypeRef>();
|
||||
|
||||
async function setTypeForLocation(loc: Location, t: TypeRef): Promise<void> {
|
||||
const maybeRef = await typeForCanonicalRef.get(loc.canonicalRef);
|
||||
if (maybeRef !== undefined) {
|
||||
assert(maybeRef === t, "Trying to set path again to different type");
|
||||
}
|
||||
typeForPath = typeForPath.set(path, t);
|
||||
typeForCanonicalRef = typeForCanonicalRef.set(loc.canonicalRef, t);
|
||||
}
|
||||
|
||||
function makeClass(path: Ref, attributes: TypeAttributes, properties: StringMap, requiredArray: string[]): TypeRef {
|
||||
const required = Set(requiredArray);
|
||||
async function makeClass(
|
||||
loc: Location,
|
||||
attributes: TypeAttributes,
|
||||
properties: StringMap,
|
||||
requiredArray: string[],
|
||||
additionalPropertiesType: boolean | TypeRef
|
||||
): Promise<TypeRef> {
|
||||
const required = OrderedSet(requiredArray);
|
||||
const propertiesMap = Map(properties);
|
||||
const propertyDescriptions = propertiesMap
|
||||
.map(propSchema => {
|
||||
|
@ -274,212 +493,259 @@ export function addTypesInSchema(typeBuilder: TypeGraphBuilder, rootJson: any, r
|
|||
// FIXME: We're using a Map instead of an OrderedMap here because we represent
|
||||
// the JSON Schema as a JavaScript object, which has no map ordering. Ideally
|
||||
// we would use a JSON parser that preserves order.
|
||||
const props = propertiesMap.map((propSchema, propName) => {
|
||||
const t = toType(
|
||||
let props = (await mapSync(propertiesMap, async (propSchema, propName) => {
|
||||
const t = await toType(
|
||||
checkStringMap(propSchema),
|
||||
path.push("properties", propName),
|
||||
loc.push("properties", propName),
|
||||
makeNamesTypeAttributes(pluralize.singular(propName), true)
|
||||
);
|
||||
const isOptional = !required.has(propName);
|
||||
return new ClassProperty(t, isOptional);
|
||||
});
|
||||
return typeBuilder.getUniqueClassType(attributes, true, props.toOrderedMap());
|
||||
}
|
||||
})).toOrderedMap();
|
||||
const additionalRequired = required.subtract(props.keySeq());
|
||||
if (!additionalRequired.isEmpty()) {
|
||||
let t: TypeRef;
|
||||
if (additionalPropertiesType === false) {
|
||||
return panic("Can't have non-specified required properties but forbidden additionalTypes");
|
||||
}
|
||||
if (additionalPropertiesType === true) {
|
||||
t = typeBuilder.getPrimitiveType("any");
|
||||
} else {
|
||||
t = additionalPropertiesType;
|
||||
}
|
||||
|
||||
function makeMap(path: Ref, typeAttributes: TypeAttributes, additional: StringMap): TypeRef {
|
||||
path = path.push("additionalProperties");
|
||||
const valuesType = toType(additional, path, singularizeTypeNames(typeAttributes));
|
||||
return typeBuilder.getMapType(valuesType);
|
||||
}
|
||||
|
||||
function fromTypeName(schema: StringMap, path: Ref, typeAttributes: TypeAttributes, typeName: string): TypeRef {
|
||||
// FIXME: We seem to be overzealous in making attributes. We get them from
|
||||
// our caller, then we make them again here, and then we make them again
|
||||
// in `makeClass`, potentially in other places, too.
|
||||
typeAttributes = makeAttributes(schema, path, makeTypeAttributesInferred(typeAttributes));
|
||||
switch (typeName) {
|
||||
case "object":
|
||||
let required: string[];
|
||||
if (schema.required === undefined) {
|
||||
required = [];
|
||||
} else {
|
||||
required = checkStringArray(schema.required);
|
||||
}
|
||||
|
||||
// FIXME: Don't put type attributes in the union AND its members.
|
||||
const unionType = typeBuilder.getUniqueUnionType(typeAttributes, undefined);
|
||||
setTypeForPath(path, unionType);
|
||||
|
||||
const typesInUnion: TypeRef[] = [];
|
||||
|
||||
if (schema.properties !== undefined) {
|
||||
typesInUnion.push(makeClass(path, typeAttributes, checkStringMap(schema.properties), required));
|
||||
}
|
||||
|
||||
if (schema.additionalProperties !== undefined) {
|
||||
const additional = schema.additionalProperties;
|
||||
// FIXME: We don't treat `additional === true`, which is also the default,
|
||||
// not according to spec. It should be translated into a map type to any,
|
||||
// though that's not what the intention usually is. Ideally, we'd find a
|
||||
// way to store additional attributes on regular classes.
|
||||
if (additional === false) {
|
||||
if (schema.properties === undefined) {
|
||||
typesInUnion.push(makeClass(path, typeAttributes, {}, required));
|
||||
}
|
||||
} else if (typeof additional === "object") {
|
||||
typesInUnion.push(makeMap(path, typeAttributes, checkStringMap(additional)));
|
||||
}
|
||||
}
|
||||
|
||||
if (typesInUnion.length === 0) {
|
||||
typesInUnion.push(typeBuilder.getMapType(typeBuilder.getPrimitiveType("any")));
|
||||
}
|
||||
typeBuilder.setSetOperationMembers(unionType, OrderedSet(typesInUnion));
|
||||
return unionType;
|
||||
case "array":
|
||||
if (schema.items !== undefined) {
|
||||
path = path.push("items");
|
||||
return typeBuilder.getArrayType(
|
||||
toType(checkStringMap(schema.items), path, singularizeTypeNames(typeAttributes))
|
||||
);
|
||||
}
|
||||
return typeBuilder.getArrayType(typeBuilder.getPrimitiveType("any"));
|
||||
case "boolean":
|
||||
return typeBuilder.getPrimitiveType("bool");
|
||||
case "string":
|
||||
if (schema.format !== undefined) {
|
||||
switch (schema.format) {
|
||||
case "date":
|
||||
return typeBuilder.getPrimitiveType("date");
|
||||
case "time":
|
||||
return typeBuilder.getPrimitiveType("time");
|
||||
case "date-time":
|
||||
return typeBuilder.getPrimitiveType("date-time");
|
||||
default:
|
||||
// FIXME: Output a warning here instead to indicate that
|
||||
// the format is uninterpreted.
|
||||
return typeBuilder.getStringType(typeAttributes, undefined);
|
||||
}
|
||||
}
|
||||
return typeBuilder.getStringType(typeAttributes, undefined);
|
||||
case "null":
|
||||
return typeBuilder.getPrimitiveType("null");
|
||||
case "integer":
|
||||
return typeBuilder.getPrimitiveType("integer");
|
||||
case "number":
|
||||
return typeBuilder.getPrimitiveType("double");
|
||||
default:
|
||||
return panic(`not a type name: ${typeName}`);
|
||||
const additionalProps = additionalRequired.toOrderedMap().map(_name => new ClassProperty(t, true));
|
||||
props = props.merge(additionalProps);
|
||||
}
|
||||
return typeBuilder.getUniqueClassType(attributes, true, props);
|
||||
}
|
||||
|
||||
function convertToType(schema: StringMap, path: Ref, typeAttributes: TypeAttributes): TypeRef {
|
||||
typeAttributes = makeAttributes(schema, path, typeAttributes);
|
||||
async function makeMap(loc: Location, typeAttributes: TypeAttributes, additional: StringMap): Promise<[TypeRef, TypeRef]> {
|
||||
loc = loc.push("additionalProperties");
|
||||
const valuesType = await toType(additional, loc, singularizeTypeNames(typeAttributes));
|
||||
return [typeBuilder.getMapType(valuesType), valuesType];
|
||||
}
|
||||
|
||||
function makeTypesFromCases(
|
||||
cases: any,
|
||||
kind: string
|
||||
): TypeRef[] {
|
||||
async function convertToType(schema: StringMap, loc: Location, typeAttributes: TypeAttributes): Promise<TypeRef> {
|
||||
typeAttributes = makeAttributes(schema, loc, typeAttributes);
|
||||
const inferredAttributes = makeTypeAttributesInferred(typeAttributes);
|
||||
|
||||
function makeStringType(): TypeRef {
|
||||
if (schema.format !== undefined) {
|
||||
switch (schema.format) {
|
||||
case "date":
|
||||
return typeBuilder.getPrimitiveType("date");
|
||||
case "time":
|
||||
return typeBuilder.getPrimitiveType("time");
|
||||
case "date-time":
|
||||
return typeBuilder.getPrimitiveType("date-time");
|
||||
default:
|
||||
// FIXME: Output a warning here instead to indicate that
|
||||
// the format is uninterpreted.
|
||||
return typeBuilder.getStringType(inferredAttributes, undefined);
|
||||
}
|
||||
}
|
||||
return typeBuilder.getStringType(inferredAttributes, undefined);
|
||||
}
|
||||
|
||||
async function makeArrayType(): Promise<TypeRef> {
|
||||
if (schema.items !== undefined) {
|
||||
loc = loc.push("items");
|
||||
return typeBuilder.getArrayType(
|
||||
await toType(checkStringMap(schema.items), loc, singularizeTypeNames(typeAttributes))
|
||||
);
|
||||
}
|
||||
return typeBuilder.getArrayType(typeBuilder.getPrimitiveType("any"));
|
||||
}
|
||||
|
||||
async function makeObjectTypes(): Promise<TypeRef[]> {
|
||||
let required: string[];
|
||||
if (schema.required === undefined) {
|
||||
required = [];
|
||||
} else {
|
||||
required = checkStringArray(schema.required);
|
||||
}
|
||||
|
||||
const typesInUnion: TypeRef[] = [];
|
||||
|
||||
let additionalPropertiesType: boolean | TypeRef = true;
|
||||
if (schema.additionalProperties !== undefined) {
|
||||
const additional = schema.additionalProperties;
|
||||
// FIXME: We don't treat `additional === true`, which is also the default,
|
||||
// not according to spec. It should be translated into a map type to any,
|
||||
// though that's not what the intention usually is. Ideally, we'd find a
|
||||
// way to store additional attributes on regular classes.
|
||||
if (additional === false) {
|
||||
if (schema.properties === undefined) {
|
||||
typesInUnion.push(await makeClass(loc, inferredAttributes, {}, required, false));
|
||||
}
|
||||
additionalPropertiesType = false;
|
||||
} else if (typeof additional === "object") {
|
||||
const [mapType, valuesType] = await makeMap(loc, inferredAttributes, checkStringMap(additional));
|
||||
typesInUnion.push(await mapType);
|
||||
additionalPropertiesType = valuesType;
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.properties !== undefined) {
|
||||
typesInUnion.push(
|
||||
await makeClass(loc, inferredAttributes, checkStringMap(schema.properties), required, additionalPropertiesType)
|
||||
);
|
||||
}
|
||||
|
||||
if (typesInUnion.length === 0) {
|
||||
typesInUnion.push(typeBuilder.getMapType(typeBuilder.getPrimitiveType("any")));
|
||||
}
|
||||
return typesInUnion;
|
||||
}
|
||||
|
||||
async function makeTypesFromCases(cases: any, kind: string): Promise<TypeRef[]> {
|
||||
if (!Array.isArray(cases)) {
|
||||
return panic(`Cases are not an array: ${cases}`);
|
||||
}
|
||||
// FIXME: This cast shouldn't be necessary, but TypeScript forces our hand.
|
||||
return cases.map((t, index) =>
|
||||
toType(checkStringMap(t), path.push(kind, index.toString()), makeTypeAttributesInferred(typeAttributes))
|
||||
return await mapSync(
|
||||
cases,
|
||||
async (t, index) =>
|
||||
await toType(
|
||||
checkStringMap(t),
|
||||
loc.push(kind, index.toString()),
|
||||
makeTypeAttributesInferred(typeAttributes)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function convertOneOrAnyOf(cases: any, kind: string): TypeRef {
|
||||
async function convertOneOrAnyOf(cases: any, kind: string): Promise<TypeRef> {
|
||||
const unionType = typeBuilder.getUniqueUnionType(makeTypeAttributesInferred(typeAttributes), undefined);
|
||||
typeBuilder.setSetOperationMembers(unionType, OrderedSet(makeTypesFromCases(cases, kind)));
|
||||
typeBuilder.setSetOperationMembers(unionType, OrderedSet(await makeTypesFromCases(cases, kind)));
|
||||
return unionType;
|
||||
}
|
||||
|
||||
if (schema.$ref !== undefined) {
|
||||
const ref = Ref.parse(schema.$ref);
|
||||
const target = ref.lookupRef(root);
|
||||
const attributes = modifyTypeNames(typeAttributes, tn => {
|
||||
if (!defined(tn).areInferred) return tn;
|
||||
return new TypeNames(OrderedSet([ref.name]), OrderedSet(), true);
|
||||
});
|
||||
return toType(target, ref, attributes);
|
||||
} else if (Array.isArray(schema.enum)) {
|
||||
let cases = schema.enum as any[];
|
||||
const haveNull = cases.indexOf(null) >= 0;
|
||||
cases = cases.filter(c => c !== null);
|
||||
if (cases.filter(c => typeof c !== "string").length > 0) {
|
||||
return panic(`Non-string enum cases are not supported, at ${path.toString()}`);
|
||||
const enumArray = Array.isArray(schema.enum) ? schema.enum : undefined;
|
||||
const typeSet = schema.type !== undefined ? checkTypeList(schema.type) : undefined;
|
||||
|
||||
function includePrimitiveType(name: string): boolean {
|
||||
if (typeSet !== undefined && !typeSet.has(name)) {
|
||||
return false;
|
||||
}
|
||||
const tref = typeBuilder.getEnumType(typeAttributes, OrderedSet(checkStringArray(cases)));
|
||||
if (haveNull) {
|
||||
return typeBuilder.getUnionType(
|
||||
typeAttributes,
|
||||
OrderedSet([tref, typeBuilder.getPrimitiveType("null")])
|
||||
);
|
||||
} else {
|
||||
return tref;
|
||||
if (enumArray !== undefined) {
|
||||
let predicate: (x: any) => boolean;
|
||||
switch (name) {
|
||||
case "null":
|
||||
predicate = (x: any) => x === null;
|
||||
break;
|
||||
case "integer":
|
||||
predicate = (x: any) => typeof x === "number" && x === Math.floor(x)
|
||||
break;
|
||||
default:
|
||||
predicate = (x: any) => typeof x === name;
|
||||
break;
|
||||
}
|
||||
|
||||
return enumArray.find(predicate) !== undefined;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
let jsonTypes: OrderedSet<string> | undefined = undefined;
|
||||
if (schema.type !== undefined) {
|
||||
jsonTypes = checkTypeList(schema.type);
|
||||
} else if (schema.properties !== undefined || schema.additionalProperties !== undefined) {
|
||||
jsonTypes = OrderedSet(["object"]);
|
||||
}
|
||||
const includeObject = enumArray === undefined && (typeSet === undefined || typeSet.has("object"));
|
||||
const includeArray = enumArray === undefined && (typeSet === undefined || typeSet.has("array"));
|
||||
const needStringEnum = includePrimitiveType("string") && enumArray !== undefined && enumArray.find((x: any) => typeof x === "string") !== undefined;
|
||||
const needUnion = typeSet !== undefined || schema.properties !== undefined || schema.additionalProperties !== undefined || schema.items !== undefined || enumArray !== undefined;
|
||||
|
||||
const intersectionType = typeBuilder.getUniqueIntersectionType(typeAttributes, undefined);
|
||||
setTypeForPath(path, intersectionType);
|
||||
await setTypeForLocation(loc, intersectionType);
|
||||
const types: TypeRef[] = [];
|
||||
if (schema.allOf !== undefined) {
|
||||
types.push(...makeTypesFromCases(schema.allOf, "allOf"));
|
||||
}
|
||||
if (schema.oneOf) {
|
||||
types.push(convertOneOrAnyOf(schema.oneOf, "oneOf"));
|
||||
}
|
||||
if (schema.anyOf) {
|
||||
types.push(convertOneOrAnyOf(schema.anyOf, "anyOf"));
|
||||
}
|
||||
if (jsonTypes !== undefined) {
|
||||
if (jsonTypes.size === 1) {
|
||||
types.push(fromTypeName(schema, path.pushObject(), typeAttributes, defined(jsonTypes.first())));
|
||||
} else {
|
||||
const unionType = typeBuilder.getUniqueUnionType(typeAttributes, undefined);
|
||||
const unionTypes = jsonTypes
|
||||
.toList()
|
||||
.map((n, index) => fromTypeName(schema, path.pushType(index), typeAttributes, n));
|
||||
typeBuilder.setSetOperationMembers(unionType, OrderedSet(unionTypes));
|
||||
types.push(unionType);
|
||||
|
||||
if (needUnion) {
|
||||
const unionTypes: TypeRef[] = [];
|
||||
|
||||
for (const [name, kind] of [["null", "null"], ["number", "double"], ["integer", "integer"], ["boolean", "bool"]] as [string, PrimitiveTypeKind][]) {
|
||||
if (!includePrimitiveType(name)) continue;
|
||||
|
||||
unionTypes.push(typeBuilder.getPrimitiveType(kind));
|
||||
}
|
||||
|
||||
if (needStringEnum) {
|
||||
let cases = enumArray as any[];
|
||||
cases = cases.filter(x => typeof x === "string");
|
||||
unionTypes.push(typeBuilder.getEnumType(inferredAttributes, OrderedSet(cases)));
|
||||
} else if (includePrimitiveType("string")) {
|
||||
unionTypes.push(makeStringType());
|
||||
}
|
||||
|
||||
if (includeArray) {
|
||||
unionTypes.push(await makeArrayType());
|
||||
}
|
||||
if (includeObject) {
|
||||
unionTypes.push(...await makeObjectTypes())
|
||||
}
|
||||
|
||||
types.push(typeBuilder.getUniqueUnionType(inferredAttributes, OrderedSet(unionTypes)));
|
||||
}
|
||||
|
||||
if (schema.$ref !== undefined) {
|
||||
const virtualRef = Ref.parse(schema.$ref);
|
||||
const [target, newLoc] = await resolveVirtualRef(loc, virtualRef);
|
||||
const attributes = modifyTypeNames(typeAttributes, tn => {
|
||||
if (!defined(tn).areInferred) return tn;
|
||||
return new TypeNames(OrderedSet([newLoc.canonicalRef.name]), OrderedSet(), true);
|
||||
});
|
||||
types.push(await toType(target, newLoc, attributes));
|
||||
}
|
||||
|
||||
if (schema.allOf !== undefined) {
|
||||
types.push(...(await makeTypesFromCases(schema.allOf, "allOf")));
|
||||
}
|
||||
if (schema.oneOf !== undefined) {
|
||||
types.push(await convertOneOrAnyOf(schema.oneOf, "oneOf"));
|
||||
}
|
||||
if (schema.anyOf !== undefined) {
|
||||
types.push(await convertOneOrAnyOf(schema.anyOf, "anyOf"));
|
||||
}
|
||||
|
||||
typeBuilder.setSetOperationMembers(intersectionType, OrderedSet(types));
|
||||
return intersectionType;
|
||||
}
|
||||
|
||||
function toType(schema: StringMap, path: Ref, typeAttributes: TypeAttributes): TypeRef {
|
||||
const maybeType = typeForPath.get(path);
|
||||
async function toType(schema: JSONSchema, loc: Location, typeAttributes: TypeAttributes): Promise<TypeRef> {
|
||||
const maybeType = typeForCanonicalRef.get(loc.canonicalRef);
|
||||
if (maybeType !== undefined) {
|
||||
return maybeType;
|
||||
}
|
||||
const result = convertToType(schema, path, typeAttributes);
|
||||
setTypeForPath(path, result);
|
||||
|
||||
let result: TypeRef;
|
||||
if (typeof schema === "boolean") {
|
||||
// FIXME: Empty union. We'd have to check that it's supported everywhere,
|
||||
// in particular in union flattening.
|
||||
assert(schema === true, 'Schema "false" is not supported');
|
||||
result = typeBuilder.getPrimitiveType("any");
|
||||
} else {
|
||||
loc = loc.updateWithID(schema["$id"]);
|
||||
result = await convertToType(schema, loc, typeAttributes);
|
||||
}
|
||||
|
||||
await setTypeForLocation(loc, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
references.forEach((topLevelRef, topLevelName) => {
|
||||
const target = topLevelRef.lookupRef(root);
|
||||
const t = toType(target, topLevelRef, makeNamesTypeAttributes(topLevelName, false));
|
||||
await forEachSync(references, async (topLevelRef, topLevelName) => {
|
||||
const [target, loc] = await resolveVirtualRef(undefined, topLevelRef);
|
||||
const t = await toType(target, loc, makeNamesTypeAttributes(topLevelName, false));
|
||||
typeBuilder.addTopLevel(topLevelName, t);
|
||||
});
|
||||
}
|
||||
|
||||
export function definitionRefsInSchema(rootJson: any): Map<string, Ref> {
|
||||
if (typeof rootJson !== "object") return Map();
|
||||
const definitions = rootJson.definitions;
|
||||
export async function definitionRefsInSchema(store: JSONSchemaStore, address: string): Promise<Map<string, Ref>> {
|
||||
const ref = Ref.parse(address);
|
||||
const rootSchema = await store.get(ref.address);
|
||||
const schema = ref.lookupRef(rootSchema);
|
||||
if (typeof schema !== "object") return Map();
|
||||
const definitions = schema.definitions;
|
||||
if (typeof definitions !== "object") return Map();
|
||||
const definitionsRef = ref.push("definitions");
|
||||
return Map(
|
||||
Object.keys(definitions).map(name => {
|
||||
return [name, Ref.root.push("definitions", name)] as [string, Ref];
|
||||
Object.getOwnPropertyNames(definitions).map(name => {
|
||||
return [name, definitionsRef.push(name)] as [string, Ref];
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
"use strict";
|
||||
|
||||
import * as fs from "fs";
|
||||
import { Readable } from "stream";
|
||||
import { getStream } from "./get-stream/index";
|
||||
|
||||
import { JSONSchemaStore, JSONSchema } from "./JSONSchemaInput";
|
||||
import { panic } from "./Support";
|
||||
|
||||
// The typings for this module are screwy
|
||||
const isURL = require("is-url");
|
||||
const fetch = require("node-fetch");
|
||||
|
||||
export async function readableFromFileOrURL(fileOrUrl: string): Promise<Readable> {
|
||||
if (isURL(fileOrUrl)) {
|
||||
const response = await fetch(fileOrUrl);
|
||||
return response.body;
|
||||
} else if (fs.existsSync(fileOrUrl)) {
|
||||
return fs.createReadStream(fileOrUrl);
|
||||
} else {
|
||||
return panic(`Input file ${fileOrUrl} does not exist`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function readFromFileOrURL(fileOrURL: string): Promise<string> {
|
||||
return await getStream(await readableFromFileOrURL(fileOrURL));
|
||||
}
|
||||
|
||||
export class FetchingJSONSchemaStore extends JSONSchemaStore {
|
||||
async fetch(address: string): Promise<JSONSchema | undefined> {
|
||||
// console.log(`Fetching ${address}`);
|
||||
return JSON.parse(await readFromFileOrURL(address));
|
||||
}
|
||||
}
|
|
@ -22,7 +22,6 @@ import {
|
|||
UnionType,
|
||||
PrimitiveStringTypeKind,
|
||||
PrimitiveTypeKind,
|
||||
PrimitiveType,
|
||||
StringType,
|
||||
ArrayType,
|
||||
matchTypeExhaustive,
|
||||
|
@ -46,12 +45,12 @@ function intersectionMembersRecursively(intersection: IntersectionType): [Ordere
|
|||
let attributes = emptyTypeAttributes;
|
||||
function process(t: Type): void {
|
||||
if (t instanceof IntersectionType) {
|
||||
attributes = combineTypeAttributes([attributes, t.getAttributes()]);
|
||||
attributes = combineTypeAttributes(attributes, t.getAttributes());
|
||||
t.members.forEach(process);
|
||||
} else if (t.kind !== "any") {
|
||||
types.push(t);
|
||||
} else {
|
||||
attributes = combineTypeAttributes([attributes, t.getAttributes()]);
|
||||
attributes = combineTypeAttributes(attributes, t.getAttributes());
|
||||
}
|
||||
}
|
||||
process(intersection);
|
||||
|
@ -64,6 +63,10 @@ function canResolve(t: IntersectionType): boolean {
|
|||
return members.every(m => !(m instanceof UnionType) || m.isCanonical);
|
||||
}
|
||||
|
||||
function attributesForTypes<T extends TypeKind>(types: OrderedSet<Type>): TypeAttributeMap<T> {
|
||||
return types.toMap().map(t => t.getAttributes()).mapKeys(t => t.kind) as Map<T, TypeAttributes>;
|
||||
}
|
||||
|
||||
class IntersectionAccumulator
|
||||
implements
|
||||
UnionTypeProvider<
|
||||
|
@ -72,12 +75,19 @@ class IntersectionAccumulator
|
|||
OrderedSet<Type> | undefined
|
||||
> {
|
||||
private _primitiveStringTypes: OrderedSet<PrimitiveStringTypeKind> | undefined;
|
||||
private _primitiveStringAttributes: TypeAttributeMap<PrimitiveStringTypeKind> = OrderedMap();
|
||||
|
||||
private _otherPrimitiveTypes: OrderedSet<PrimitiveTypeKind> | undefined;
|
||||
private _otherPrimitiveAttributes: TypeAttributeMap<PrimitiveTypeKind> = OrderedMap();
|
||||
|
||||
private _enumCases: OrderedSet<string> | undefined;
|
||||
private _enumAttributes: TypeAttributes = emptyTypeAttributes;
|
||||
|
||||
// * undefined: We haven't seen any types yet.
|
||||
// * OrderedSet: All types we've seen can be arrays.
|
||||
// * false: At least one of the types seen can't be an array.
|
||||
private _arrayItemTypes: OrderedSet<Type> | undefined | false;
|
||||
private _arrayAttributes: TypeAttributes = emptyTypeAttributes;
|
||||
|
||||
// We allow only either maps, classes, or neither. States:
|
||||
//
|
||||
|
@ -93,16 +103,26 @@ class IntersectionAccumulator
|
|||
// are both undefined.
|
||||
|
||||
private _mapValueTypes: OrderedSet<Type> | undefined = OrderedSet();
|
||||
private _mapAttributes: TypeAttributes = emptyTypeAttributes;
|
||||
|
||||
private _classProperties: OrderedMap<string, GenericClassProperty<OrderedSet<Type>>> | undefined;
|
||||
private _classAttributes: TypeAttributes = emptyTypeAttributes;
|
||||
|
||||
private _lostTypeAttributes: boolean = false;
|
||||
|
||||
private updatePrimitiveStringTypes(members: OrderedSet<Type>): void {
|
||||
const types = members.filter(t => isPrimitiveStringTypeKind(t.kind));
|
||||
const attributes = attributesForTypes<PrimitiveStringTypeKind>(types);
|
||||
this._primitiveStringAttributes = this._primitiveStringAttributes.mergeWith(combineTypeAttributes, attributes);
|
||||
|
||||
const kinds = types.map(t => t.kind) as OrderedSet<PrimitiveStringTypeKind>;
|
||||
if (this._primitiveStringTypes === undefined) {
|
||||
this._primitiveStringTypes = kinds;
|
||||
return;
|
||||
}
|
||||
|
||||
// If the unrestricted string type is part of the union, this doesn't add
|
||||
// any more restrictions.
|
||||
if (members.find(t => t instanceof StringType) === undefined) {
|
||||
this._primitiveStringTypes = this._primitiveStringTypes.intersect(kinds);
|
||||
}
|
||||
|
@ -110,6 +130,9 @@ class IntersectionAccumulator
|
|||
|
||||
private updateOtherPrimitiveTypes(members: OrderedSet<Type>): void {
|
||||
const types = members.filter(t => isPrimitiveTypeKind(t.kind) && !isPrimitiveStringTypeKind(t.kind));
|
||||
const attributes = attributesForTypes<PrimitiveTypeKind>(types);
|
||||
this._otherPrimitiveAttributes = this._otherPrimitiveAttributes.mergeWith(combineTypeAttributes, attributes);
|
||||
|
||||
const kinds = types.map(t => t.kind) as OrderedSet<PrimitiveStringTypeKind>;
|
||||
if (this._otherPrimitiveTypes === undefined) {
|
||||
this._otherPrimitiveTypes = kinds;
|
||||
|
@ -128,11 +151,14 @@ class IntersectionAccumulator
|
|||
}
|
||||
|
||||
private updateEnumCases(members: OrderedSet<Type>): void {
|
||||
const enums = members.filter(t => t instanceof EnumType) as OrderedSet<EnumType>;
|
||||
const attributes = combineTypeAttributes(enums.map(t => t.getAttributes()).toArray());
|
||||
this._enumAttributes = combineTypeAttributes(this._enumAttributes, attributes);
|
||||
if (members.find(t => t instanceof StringType) !== undefined) {
|
||||
return;
|
||||
}
|
||||
const newCases = OrderedSet<string>().union(
|
||||
...members.map(t => (t instanceof EnumType ? t.cases : OrderedSet<string>())).toArray()
|
||||
...enums.map(t => t.cases).toArray()
|
||||
);
|
||||
if (this._enumCases === undefined) {
|
||||
this._enumCases = newCases;
|
||||
|
@ -142,18 +168,19 @@ class IntersectionAccumulator
|
|||
}
|
||||
|
||||
private updateArrayItemTypes(members: OrderedSet<Type>): void {
|
||||
if (this._arrayItemTypes === false) return;
|
||||
|
||||
const maybeArray = members.find(t => t instanceof ArrayType) as ArrayType | undefined;
|
||||
if (maybeArray === undefined) {
|
||||
this._arrayItemTypes = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this._arrayAttributes = combineTypeAttributes(this._arrayAttributes, maybeArray.getAttributes());
|
||||
|
||||
if (this._arrayItemTypes === undefined) {
|
||||
this._arrayItemTypes = OrderedSet();
|
||||
} else if (this._arrayItemTypes !== false) {
|
||||
this._arrayItemTypes = this._arrayItemTypes.add(maybeArray.items);
|
||||
}
|
||||
this._arrayItemTypes = this._arrayItemTypes.add(maybeArray.items);
|
||||
}
|
||||
|
||||
private updateMapValueTypesAndClassProperties(members: OrderedSet<Type>): void {
|
||||
|
@ -169,6 +196,13 @@ class IntersectionAccumulator
|
|||
"Can't have both class and map type in a canonical union"
|
||||
);
|
||||
|
||||
if (maybeClass !== undefined) {
|
||||
this._classAttributes = combineTypeAttributes(this._classAttributes, maybeClass.getAttributes());
|
||||
}
|
||||
if (maybeMap !== undefined) {
|
||||
this._mapAttributes = combineTypeAttributes(this._mapAttributes, maybeMap.getAttributes());
|
||||
}
|
||||
|
||||
if (maybeMap === undefined && maybeClass === undefined) {
|
||||
// Moving to state 4.
|
||||
this._mapValueTypes = undefined;
|
||||
|
@ -188,6 +222,7 @@ class IntersectionAccumulator
|
|||
|
||||
this._mapValueTypes = undefined;
|
||||
this._classProperties = makeProperties();
|
||||
this._lostTypeAttributes = true;
|
||||
}
|
||||
} else if (this._classProperties !== undefined) {
|
||||
// We're in state 3.
|
||||
|
@ -207,6 +242,7 @@ class IntersectionAccumulator
|
|||
}
|
||||
} else {
|
||||
// We're in state 4. No way out of state 4.
|
||||
this._lostTypeAttributes = true;
|
||||
}
|
||||
|
||||
assert(
|
||||
|
@ -215,11 +251,6 @@ class IntersectionAccumulator
|
|||
);
|
||||
}
|
||||
|
||||
private addAny(_t: PrimitiveType): void {
|
||||
// "any" doesn't change the types at all
|
||||
// FIXME: just add attributes
|
||||
}
|
||||
|
||||
private addUnionSet(members: OrderedSet<Type>): void {
|
||||
this.updatePrimitiveStringTypes(members);
|
||||
this.updateOtherPrimitiveTypes(members);
|
||||
|
@ -228,22 +259,16 @@ class IntersectionAccumulator
|
|||
this.updateMapValueTypesAndClassProperties(members);
|
||||
}
|
||||
|
||||
private addUnion(u: UnionType): void {
|
||||
this.addUnionSet(u.members);
|
||||
}
|
||||
|
||||
addType(t: Type): TypeAttributes {
|
||||
// FIXME: We're very lazy here. We're supposed to keep type
|
||||
// attributes separately for each type kind, but we collect
|
||||
// them all together and return them as attributes for the
|
||||
// overall result type.
|
||||
let attributes = t.getAttributes();
|
||||
matchTypeExhaustive<void>(
|
||||
t,
|
||||
_noneType => {
|
||||
return panic("There shouldn't be a none type");
|
||||
},
|
||||
anyType => this.addAny(anyType),
|
||||
_anyType => {
|
||||
return panic("The any type should have been filtered out in intersectionMembersRecursively")
|
||||
},
|
||||
nullType => this.addUnionSet(OrderedSet([nullType])),
|
||||
boolType => this.addUnionSet(OrderedSet([boolType])),
|
||||
integerType => this.addUnionSet(OrderedSet([integerType])),
|
||||
|
@ -253,12 +278,15 @@ class IntersectionAccumulator
|
|||
classType => this.addUnionSet(OrderedSet([classType])),
|
||||
mapType => this.addUnionSet(OrderedSet([mapType])),
|
||||
enumType => this.addUnionSet(OrderedSet([enumType])),
|
||||
unionType => this.addUnion(unionType),
|
||||
unionType => {
|
||||
attributes = combineTypeAttributes([attributes].concat(unionType.members.map(t => t.getAttributes()).toArray()));
|
||||
this.addUnionSet(unionType.members);
|
||||
},
|
||||
dateType => this.addUnionSet(OrderedSet([dateType])),
|
||||
timeType => this.addUnionSet(OrderedSet([timeType])),
|
||||
dateTimeType => this.addUnionSet(OrderedSet([dateTimeType]))
|
||||
);
|
||||
return attributes;
|
||||
return makeTypeAttributesInferred(attributes);
|
||||
}
|
||||
|
||||
get arrayData(): OrderedSet<Type> {
|
||||
|
@ -287,23 +315,55 @@ class IntersectionAccumulator
|
|||
}
|
||||
|
||||
getMemberKinds(): TypeAttributeMap<TypeKind> {
|
||||
let kinds: OrderedSet<TypeKind> = defined(this._primitiveStringTypes).union(defined(this._otherPrimitiveTypes));
|
||||
let primitiveStringKinds = defined(this._primitiveStringTypes).toOrderedMap().map(k => defined(this._primitiveStringAttributes.get(k)));
|
||||
const maybeStringAttributes = this._primitiveStringAttributes.get("string");
|
||||
// If full string was eliminated, add its attribute to the other string types
|
||||
if (maybeStringAttributes !== undefined && !primitiveStringKinds.has("string")) {
|
||||
primitiveStringKinds = primitiveStringKinds.map(a => combineTypeAttributes(a, maybeStringAttributes));
|
||||
}
|
||||
|
||||
let otherPrimitiveKinds = defined(this._otherPrimitiveTypes).toOrderedMap().map(k => defined(this._otherPrimitiveAttributes.get(k)));
|
||||
const maybeDoubleAttributes = this._otherPrimitiveAttributes.get("double");
|
||||
// If double was eliminated, add its attributes to integer
|
||||
if (maybeDoubleAttributes !== undefined && !otherPrimitiveKinds.has("double")) {
|
||||
otherPrimitiveKinds = otherPrimitiveKinds.map((a, k) => {
|
||||
if (k !== "integer") return a;
|
||||
return combineTypeAttributes(a, maybeDoubleAttributes);
|
||||
});
|
||||
}
|
||||
|
||||
let kinds: TypeAttributeMap<TypeKind> = primitiveStringKinds.merge(otherPrimitiveKinds);
|
||||
|
||||
if (this._enumCases !== undefined && this._enumCases.size > 0) {
|
||||
kinds = kinds.add("enum");
|
||||
kinds = kinds.set("enum", this._enumAttributes);
|
||||
} else if (!this._enumAttributes.isEmpty()) {
|
||||
if (kinds.has("string")) {
|
||||
kinds = kinds.update("string", ta => combineTypeAttributes(ta, this._enumAttributes));
|
||||
} else {
|
||||
this._lostTypeAttributes = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (OrderedSet.isOrderedSet(this._arrayItemTypes)) {
|
||||
kinds = kinds.add("array");
|
||||
kinds = kinds.set("array", this._arrayAttributes);
|
||||
} else if (!this._arrayAttributes.isEmpty()) {
|
||||
this._lostTypeAttributes = true;
|
||||
}
|
||||
|
||||
const objectAttributes = combineTypeAttributes(this._classAttributes, this._mapAttributes);
|
||||
if (this._mapValueTypes !== undefined) {
|
||||
kinds = kinds.add("map");
|
||||
kinds = kinds.set("map", objectAttributes);
|
||||
} else if (this._classProperties !== undefined) {
|
||||
kinds = kinds.add("class");
|
||||
kinds = kinds.set("class", objectAttributes);
|
||||
} else if (!objectAttributes.isEmpty()) {
|
||||
this._lostTypeAttributes = true;
|
||||
}
|
||||
return kinds.toOrderedMap().map(_ => emptyTypeAttributes);
|
||||
|
||||
return kinds;
|
||||
}
|
||||
|
||||
get lostTypeAttributes(): boolean {
|
||||
return false;
|
||||
return this._lostTypeAttributes;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -405,7 +465,7 @@ export function resolveIntersections(graph: TypeGraph, stringTypeMapping: String
|
|||
const extraAttributes = makeTypeAttributesInferred(
|
||||
combineTypeAttributes(members.map(t => accumulator.addType(t)).toArray())
|
||||
);
|
||||
const attributes = combineTypeAttributes([intersectionAttributes, extraAttributes]);
|
||||
const attributes = combineTypeAttributes(intersectionAttributes, extraAttributes);
|
||||
|
||||
const unionBuilder = new IntersectionUnionBuilder(builder);
|
||||
const tref = unionBuilder.buildUnion(accumulator, true, attributes, forwardingRef);
|
||||
|
@ -425,7 +485,7 @@ export function resolveIntersections(graph: TypeGraph, stringTypeMapping: String
|
|||
return [graph, false];
|
||||
}
|
||||
const groups = resolvableIntersections.map(i => [i]).toArray();
|
||||
graph = graph.rewrite(stringTypeMapping, false, groups, replace);
|
||||
graph = graph.rewrite("resolve intersections", stringTypeMapping, false, groups, replace);
|
||||
|
||||
// console.log(`resolved ${resolvableIntersections.size} of ${intersections.size} intersections`);
|
||||
return [graph, !needsRepeat && intersections.size === resolvableIntersections.size];
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
"use strict";
|
||||
|
||||
import { Collection, List, Set } from "immutable";
|
||||
import { Collection, List, Set, isKeyed, isIndexed } from "immutable";
|
||||
|
||||
export function intercalate<T>(separator: T, items: Collection<any, T>): List<T> {
|
||||
const acc: T[] = [];
|
||||
|
@ -83,3 +83,46 @@ export function withDefault<T>(x: T | null | undefined, theDefault: T): T {
|
|||
}
|
||||
return theDefault;
|
||||
}
|
||||
|
||||
export async function forEachSync<V>(coll: V[], f: (v: V, k: number) => Promise<void>): Promise<void>;
|
||||
export async function forEachSync<K, V>(coll: Collection.Keyed<K, V>, f: (v: V, k: K) => Promise<void>): Promise<void>;
|
||||
export async function forEachSync<V>(coll: Collection.Set<V>, f: (v: V, k: V) => Promise<void>): Promise<void>;
|
||||
export async function forEachSync<V>(coll: Collection.Indexed<V>, f: (v: V, k: number) => Promise<void>): Promise<void>;
|
||||
export async function forEachSync<K, V>(coll: Collection<K, V> | V[], f: (v: V, k: K) => Promise<void>): Promise<void> {
|
||||
if (Array.isArray(coll) || isIndexed(coll)) {
|
||||
const arr = Array.isArray(coll) ? coll : (coll as Collection.Indexed<V>).toArray();
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
// If the collection is indexed, then `K` is `number`, but
|
||||
// TypeScript doesn't know this.
|
||||
await f(arr[i], i as any);
|
||||
}
|
||||
} else if (isKeyed(coll)) {
|
||||
for (const [k, v] of (coll as Collection.Keyed<K, V>).toArray()) {
|
||||
await f(v, k);
|
||||
}
|
||||
} else {
|
||||
// I don't understand why we can't directly cast to `Collection.Set`.
|
||||
for (const v of (coll as any as Collection.Set<V>).toArray()) {
|
||||
// If the collection is a set, then `K` is the same as `v`,
|
||||
// but TypeScript doesn't know this.
|
||||
await f(v, v as any);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function mapSync<V, U>(coll: V[], f: (v: V, k: number) => Promise<U>): Promise<U[]>;
|
||||
export async function mapSync<K, V, U>(coll: Collection.Keyed<K, V>, f: (v: V, k: K) => Promise<U>): Promise<Collection.Keyed<K, U>>;
|
||||
export async function mapSync<V, U>(coll: Collection.Set<V>, f: (v: V, k: V) => Promise<U>): Promise<Collection.Set<U>>;
|
||||
export async function mapSync<V, U>(coll: Collection.Indexed<V>, f: (v: V, k: number) => Promise<U>): Promise<Collection.Indexed<U>>;
|
||||
export async function mapSync<K, V, U>(coll: Collection<K, V> | V[], f: (v: V, k: K) => Promise<U>): Promise<Collection<K, U> | U[]> {
|
||||
const results: U[] = [];
|
||||
await forEachSync(coll as any, async (v, k) => {
|
||||
results.push(await f(v as any, k as any));
|
||||
});
|
||||
|
||||
let index = 0;
|
||||
if (Array.isArray(coll)) {
|
||||
return results;
|
||||
}
|
||||
return coll.map(_v => results[index++]);
|
||||
}
|
||||
|
|
|
@ -6,8 +6,9 @@ import { panic, setUnion } from "./Support";
|
|||
export class TypeAttributeKind<T> {
|
||||
public readonly combine: (a: T, b: T) => T;
|
||||
public readonly makeInferred: (a: T) => T;
|
||||
public readonly stringify: (a: T) => string | undefined;
|
||||
|
||||
constructor(readonly name: string, combine: ((a: T, b: T) => T) | undefined, makeInferred: ((a: T) => T) | undefined) {
|
||||
constructor(readonly name: string, combine: ((a: T, b: T) => T) | undefined, makeInferred: ((a: T) => T) | undefined, stringify: ((a: T) => string | undefined) | undefined) {
|
||||
if (combine === undefined) {
|
||||
combine = () => {
|
||||
return panic(`Cannot combine type attribute ${name}`);
|
||||
|
@ -21,6 +22,11 @@ export class TypeAttributeKind<T> {
|
|||
};
|
||||
}
|
||||
this.makeInferred = makeInferred;
|
||||
|
||||
if (stringify === undefined) {
|
||||
stringify = () => undefined;
|
||||
}
|
||||
this.stringify = stringify;
|
||||
}
|
||||
|
||||
makeAttributes(value: T): TypeAttributes {
|
||||
|
@ -65,10 +71,24 @@ export type TypeAttributes = Map<TypeAttributeKind<any>, any>;
|
|||
|
||||
export const emptyTypeAttributes: TypeAttributes = Map();
|
||||
|
||||
export function combineTypeAttributes(attributeArray: TypeAttributes[]): TypeAttributes {
|
||||
if (attributeArray.length === 0) return Map();
|
||||
const first = attributeArray[0];
|
||||
const rest = attributeArray.slice(1);
|
||||
export function combineTypeAttributes(attributeArray: TypeAttributes[]): TypeAttributes;
|
||||
export function combineTypeAttributes(a: TypeAttributes, b: TypeAttributes): TypeAttributes;
|
||||
export function combineTypeAttributes(firstOrArray: TypeAttributes[] | TypeAttributes, second?: TypeAttributes): TypeAttributes {
|
||||
let attributeArray: TypeAttributes[];
|
||||
let first: TypeAttributes;
|
||||
let rest: TypeAttributes[];
|
||||
if (Array.isArray(firstOrArray)) {
|
||||
attributeArray = firstOrArray;
|
||||
if (attributeArray.length === 0) return Map();
|
||||
first = attributeArray[0];
|
||||
rest = attributeArray.slice(1);
|
||||
} else {
|
||||
if (second === undefined) {
|
||||
return panic("Must have on array or two attributes");
|
||||
}
|
||||
first = firstOrArray;
|
||||
rest = [second];
|
||||
}
|
||||
return first.mergeWith((aa, ab, kind) => kind.combine(aa, ab), ...rest);
|
||||
}
|
||||
|
||||
|
@ -76,9 +96,10 @@ export function makeTypeAttributesInferred(attr: TypeAttributes): TypeAttributes
|
|||
return attr.map((value, kind) => kind.makeInferred(value));
|
||||
}
|
||||
|
||||
export const descriptionTypeAttributeKind = new TypeAttributeKind<OrderedSet<string>>("description", setUnion, a => a);
|
||||
export const descriptionTypeAttributeKind = new TypeAttributeKind<OrderedSet<string>>("description", setUnion, a => a, undefined);
|
||||
export const propertyDescriptionsTypeAttributeKind = new TypeAttributeKind<Map<string, OrderedSet<string>>>(
|
||||
"propertyDescriptions",
|
||||
(a, b) => a.mergeWith(setUnion, b),
|
||||
a => a
|
||||
a => a,
|
||||
undefined
|
||||
);
|
||||
|
|
|
@ -119,7 +119,11 @@ export class TypeRef {
|
|||
}
|
||||
}
|
||||
|
||||
export const provenanceTypeAttributeKind = new TypeAttributeKind<Set<TypeRef>>("provenance", setUnion, a => a);
|
||||
function provenanceToString(p: Set<TypeRef>): string {
|
||||
return p.map(r => r.getIndex()).toList().sort().map(i => i.toString()).join(",");
|
||||
}
|
||||
|
||||
export const provenanceTypeAttributeKind = new TypeAttributeKind<Set<TypeRef>>("provenance", setUnion, a => a, provenanceToString);
|
||||
|
||||
export type StringTypeMapping = {
|
||||
date: PrimitiveStringTypeKind;
|
||||
|
@ -218,7 +222,7 @@ export abstract class TypeBuilder {
|
|||
if (attributes === undefined) {
|
||||
attributes = Map();
|
||||
}
|
||||
this.typeAttributes[index] = combineTypeAttributes([this.typeAttributes[index], attributes]);
|
||||
this.typeAttributes[index] = combineTypeAttributes(this.typeAttributes[index], attributes);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -700,7 +704,7 @@ function addAttributes(
|
|||
newAttributes: TypeAttributes
|
||||
): TypeAttributes {
|
||||
if (accumulatorAttributes === undefined) return newAttributes;
|
||||
return combineTypeAttributes([accumulatorAttributes, newAttributes]);
|
||||
return combineTypeAttributes(accumulatorAttributes, newAttributes);
|
||||
}
|
||||
|
||||
function setAttributes<T extends TypeKind>(
|
||||
|
@ -837,20 +841,63 @@ export class UnionAccumulator<TArray, TClass, TMap> implements UnionTypeProvider
|
|||
}
|
||||
}
|
||||
|
||||
// FIXME: Move this to UnifyClasses.ts?
|
||||
export class TypeRefUnionAccumulator extends UnionAccumulator<TypeRef, TypeRef, TypeRef> {
|
||||
private _typesAdded: Set<Type> = Set();
|
||||
class FauxUnion {
|
||||
getAttributes(): TypeAttributes {
|
||||
return emptyTypeAttributes;
|
||||
}
|
||||
}
|
||||
|
||||
// There is a method analogous to this in the IntersectionAccumulator. It might
|
||||
// make sense to find a common interface.
|
||||
private addType(t: Type): TypeAttributes {
|
||||
if (this._typesAdded.has(t)) {
|
||||
function attributesForTypes(types: Set<Type>): [OrderedMap<Type, TypeAttributes>, TypeAttributes] {
|
||||
let unionsForType: OrderedMap<Type, Set<UnionType | FauxUnion>> = OrderedMap();
|
||||
let typesForUnion: Map<UnionType | FauxUnion, Set<Type>> = Map();
|
||||
let unions: OrderedSet<UnionType> = OrderedSet();
|
||||
let unionsEquivalentToRoot: Set<UnionType> = Set();
|
||||
function traverse(t: Type, path: Set<UnionType | FauxUnion>, isEquivalentToRoot: boolean): void {
|
||||
if (t instanceof UnionType) {
|
||||
unions = unions.add(t);
|
||||
if (isEquivalentToRoot) {
|
||||
unionsEquivalentToRoot = unionsEquivalentToRoot.add(t);
|
||||
}
|
||||
|
||||
path = path.add(t);
|
||||
isEquivalentToRoot = isEquivalentToRoot && t.members.size === 1;
|
||||
t.members.forEach(m => traverse(m, path, isEquivalentToRoot));
|
||||
} else {
|
||||
unionsForType = unionsForType.update(t, Set(), s => s.union(path));
|
||||
path.forEach(u => {
|
||||
typesForUnion = typesForUnion.update(u, Set(), s => s.add(t));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const rootPath = Set([new FauxUnion()]);
|
||||
types.forEach(t => traverse(t, rootPath, types.size === 1));
|
||||
|
||||
const attributesForTypes = unionsForType.map((unions, t) => {
|
||||
const singleAncestors = unions.filter(u => defined(typesForUnion.get(u)).size === 1);
|
||||
assert(singleAncestors.every(u => defined(typesForUnion.get(u)).has(t)), "We messed up bookkeeping");
|
||||
const inheritedAttributes = singleAncestors.toArray().map(u => u.getAttributes());
|
||||
return combineTypeAttributes([t.getAttributes()].concat(inheritedAttributes));
|
||||
});
|
||||
const unionAttributes = unions.toArray().map(u => {
|
||||
const types = typesForUnion.get(u);
|
||||
if (types !== undefined && types.size === 1) {
|
||||
return emptyTypeAttributes;
|
||||
}
|
||||
this._typesAdded = this._typesAdded.add(t);
|
||||
const attributes = u.getAttributes();
|
||||
if (unionsEquivalentToRoot.has(u)) {
|
||||
return attributes;
|
||||
}
|
||||
return makeTypeAttributesInferred(attributes);
|
||||
});
|
||||
return [attributesForTypes, combineTypeAttributes(unionAttributes)];
|
||||
}
|
||||
|
||||
const attributes = t.getAttributes();
|
||||
let unionAttributes: TypeAttributes | undefined = undefined;
|
||||
// FIXME: Move this to UnifyClasses.ts?
|
||||
export class TypeRefUnionAccumulator extends UnionAccumulator<TypeRef, TypeRef, TypeRef> {
|
||||
// There is a method analogous to this in the IntersectionAccumulator. It might
|
||||
// make sense to find a common interface.
|
||||
private addType(t: Type, attributes: TypeAttributes): void {
|
||||
matchTypeExhaustive(
|
||||
t,
|
||||
_noneType => {
|
||||
|
@ -876,38 +923,19 @@ export class TypeRefUnionAccumulator extends UnionAccumulator<TypeRef, TypeRef,
|
|||
// inference. JSON Schema input uses this case, however, without enum
|
||||
// inference, which is fine, but still a bit ugly.
|
||||
enumType => this.addEnumCases(enumType.cases.toOrderedMap().map(_ => 1), attributes),
|
||||
unionType => {
|
||||
unionAttributes = this.addTypes(unionType.members);
|
||||
unionAttributes = combineTypeAttributes([attributes, unionAttributes]);
|
||||
_unionType => {
|
||||
return panic("The unions should have been eliminated in attributesForTypesInUnion");
|
||||
},
|
||||
_dateType => this.addStringType("date", attributes),
|
||||
_timeType => this.addStringType("time", attributes),
|
||||
_dateTimeType => this.addStringType("date-time", attributes)
|
||||
);
|
||||
if (unionAttributes === undefined) return emptyTypeAttributes;
|
||||
return unionAttributes;
|
||||
}
|
||||
|
||||
private get numberOfAddedNonUnionTypes(): number {
|
||||
return this._typesAdded.filter(t => !(t instanceof UnionType)).size;
|
||||
}
|
||||
|
||||
addTypes(types: Set<Type>): TypeAttributes {
|
||||
let attributes: TypeAttributes = Map();
|
||||
let numTypesForWhichWeAdded = 0;
|
||||
types.forEach(t => {
|
||||
const numBefore = this.numberOfAddedNonUnionTypes;
|
||||
attributes = combineTypeAttributes([attributes, this.addType(t)]);
|
||||
if (this.numberOfAddedNonUnionTypes > numBefore) {
|
||||
numTypesForWhichWeAdded += 1;
|
||||
}
|
||||
});
|
||||
|
||||
if (numTypesForWhichWeAdded <= 1) {
|
||||
return attributes;
|
||||
} else {
|
||||
return makeTypeAttributesInferred(attributes);
|
||||
}
|
||||
const [attributesMap, unionAttributes] = attributesForTypes(types);
|
||||
attributesMap.forEach((attributes, t) => this.addType(t, attributes));
|
||||
return unionAttributes;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -973,15 +1001,15 @@ export abstract class UnionBuilder<TBuilder extends TypeBuilder, TArrayData, TCl
|
|||
typeAttributes: TypeAttributes,
|
||||
forwardingRef?: TypeRef
|
||||
): TypeRef {
|
||||
const kinds = typeProvider.getMemberKinds();
|
||||
|
||||
if (typeProvider.lostTypeAttributes) {
|
||||
this.typeBuilder.setLostTypeAttributes();
|
||||
}
|
||||
|
||||
const kinds = typeProvider.getMemberKinds();
|
||||
|
||||
if (kinds.size === 1) {
|
||||
const [[kind, memberAttributes]] = kinds.toArray();
|
||||
const allAttributes = combineTypeAttributes([typeAttributes, memberAttributes]);
|
||||
const allAttributes = combineTypeAttributes(typeAttributes, memberAttributes);
|
||||
const t = this.makeTypeOfKind(typeProvider, kind, allAttributes, forwardingRef);
|
||||
return t;
|
||||
}
|
||||
|
|
|
@ -121,6 +121,8 @@ export class TypeGraph {
|
|||
|
||||
private _parents: Set<Type>[] | undefined = undefined;
|
||||
|
||||
private _printOnRewrite: boolean = false;
|
||||
|
||||
constructor(typeBuilder: TypeBuilder, private readonly _haveProvenanceAttributes: boolean) {
|
||||
this._typeBuilder = typeBuilder;
|
||||
}
|
||||
|
@ -214,6 +216,10 @@ export class TypeGraph {
|
|||
}).reduce<Set<TypeRef>>((a, b) => a.union(b));
|
||||
}
|
||||
|
||||
setPrintOnRewrite(): void {
|
||||
this._printOnRewrite = true;
|
||||
}
|
||||
|
||||
// Each array in `replacementGroups` is a bunch of types to be replaced by a
|
||||
// single new type. `replacer` is a function that takes a group and a
|
||||
// TypeBuilder, and builds a new type with that builder that replaces the group.
|
||||
|
@ -221,6 +227,7 @@ export class TypeGraph {
|
|||
// graph, but return types in the new graph. Recursive types must be handled
|
||||
// carefully.
|
||||
rewrite<T extends Type>(
|
||||
title: string,
|
||||
stringTypeMapping: StringTypeMapping,
|
||||
alphabetizeProperties: boolean,
|
||||
replacementGroups: T[][],
|
||||
|
@ -249,11 +256,17 @@ export class TypeGraph {
|
|||
}
|
||||
}
|
||||
|
||||
if (this._printOnRewrite) {
|
||||
newGraph.setPrintOnRewrite();
|
||||
console.log(`\n# ${title}`);
|
||||
newGraph.printGraph();
|
||||
}
|
||||
|
||||
return newGraph;
|
||||
}
|
||||
|
||||
garbageCollect(alphabetizeProperties: boolean): TypeGraph {
|
||||
const newGraph = this.rewrite(NoStringTypeMapping, alphabetizeProperties, [], (_t, _b) =>
|
||||
const newGraph = this.rewrite("GC", NoStringTypeMapping, alphabetizeProperties, [], (_t, _b) =>
|
||||
panic("This shouldn't be called"), true);
|
||||
// console.log(`GC: ${defined(newGraph._types).length} types`);
|
||||
return newGraph;
|
||||
|
@ -287,10 +300,19 @@ export class TypeGraph {
|
|||
const types = defined(this._types);
|
||||
for (let i = 0; i < types.length; i++) {
|
||||
const t = types[i];
|
||||
const namesString = t.hasNames ? ` name: ${t.getCombinedName()}` : "";
|
||||
const parts: string[] = [];
|
||||
parts.push(`${t.kind}${t.hasNames ? ` ${t.getCombinedName()}` : ""}`);
|
||||
const children = t.children;
|
||||
const childrenString = children.isEmpty() ? "" : ` children: ${children.map(c => c.typeRef.getIndex()).join(",")}`;
|
||||
console.log(`${i}: ${t.kind}${namesString}${childrenString}`);
|
||||
if (!children.isEmpty()) {
|
||||
parts.push(`children ${children.map(c => c.typeRef.getIndex()).join(",")}`);
|
||||
}
|
||||
t.getAttributes().forEach((value, kind) => {
|
||||
const maybeString = kind.stringify(value);
|
||||
if (maybeString !== undefined) {
|
||||
parts.push(maybeString);
|
||||
}
|
||||
});
|
||||
console.log(`${i}: ${parts.join(" | ")}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -301,7 +323,7 @@ export function noneToAny(graph: TypeGraph, stringTypeMapping: StringTypeMapping
|
|||
return graph;
|
||||
}
|
||||
assert(noneTypes.size === 1, "Cannot have more than one none type");
|
||||
return graph.rewrite(stringTypeMapping, false, [noneTypes.toArray()], (_, builder, forwardingRef) => {
|
||||
return graph.rewrite("none to any", stringTypeMapping, false, [noneTypes.toArray()], (_, builder, forwardingRef) => {
|
||||
return builder.getPrimitiveType("any", forwardingRef);
|
||||
});
|
||||
}
|
||||
|
@ -343,7 +365,7 @@ export function optionalToNullable(graph: TypeGraph, stringTypeMapping: StringTy
|
|||
if (classesWithOptional.size === 0) {
|
||||
return graph;
|
||||
}
|
||||
return graph.rewrite(stringTypeMapping, false, replacementGroups, (setOfClass, builder, forwardingRef) => {
|
||||
return graph.rewrite("optional to nullable", stringTypeMapping, false, replacementGroups, (setOfClass, builder, forwardingRef) => {
|
||||
assert(setOfClass.size === 1);
|
||||
const c = defined(setOfClass.first());
|
||||
return rewriteClass(c, builder, forwardingRef);
|
||||
|
|
|
@ -89,6 +89,11 @@ export class TypeNames {
|
|||
singularize(): TypeNames {
|
||||
return new TypeNames(this.names.map(pluralize.singular), this._alternativeNames.map(pluralize.singular), true);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
const inferred = this.areInferred ? "inferred" : "given";
|
||||
return `${inferred} ${this.names.join(",")} (${this._alternativeNames.join(",")})`;
|
||||
}
|
||||
}
|
||||
|
||||
export function typeNamesUnion(c: Collection<any, TypeNames>): TypeNames {
|
||||
|
@ -99,7 +104,7 @@ export function typeNamesUnion(c: Collection<any, TypeNames>): TypeNames {
|
|||
return names;
|
||||
}
|
||||
|
||||
export const namesTypeAttributeKind = new TypeAttributeKind<TypeNames>("names", (a, b) => a.add(b), a => a.makeInferred());
|
||||
export const namesTypeAttributeKind = new TypeAttributeKind<TypeNames>("names", (a, b) => a.add(b), a => a.makeInferred(), a => a.toString());
|
||||
|
||||
export function modifyTypeNames(
|
||||
attributes: TypeAttributes,
|
||||
|
|
|
@ -174,7 +174,7 @@ export function unifyTypes<T extends Type>(
|
|||
|
||||
const accumulator = new TypeRefUnionAccumulator(conflateNumbers);
|
||||
const nestedAttributes = accumulator.addTypes(types);
|
||||
typeAttributes = combineTypeAttributes([typeAttributes, nestedAttributes]);
|
||||
typeAttributes = combineTypeAttributes(typeAttributes, nestedAttributes);
|
||||
|
||||
return typeBuilder.withForwardingRef(maybeForwardingRef, forwardingRef => {
|
||||
typeBuilder.registerUnion(typeRefs, forwardingRef);
|
||||
|
|
150
src/cli.ts
150
src/cli.ts
|
@ -2,9 +2,6 @@ import * as fs from "fs";
|
|||
import * as path from "path";
|
||||
import * as _ from "lodash";
|
||||
|
||||
// The typings for this module are screwy
|
||||
const isURL = require("is-url");
|
||||
|
||||
import {
|
||||
Run,
|
||||
JSONTypeSource,
|
||||
|
@ -15,8 +12,7 @@ import {
|
|||
isJSONSource,
|
||||
StringInput,
|
||||
SchemaTypeSource,
|
||||
isSchemaSource,
|
||||
isGraphQLSource
|
||||
isSchemaSource
|
||||
} from ".";
|
||||
import { OptionDefinition } from "./RendererOptions";
|
||||
import * as targetLanguages from "./Language/All";
|
||||
|
@ -29,11 +25,11 @@ import { introspectServer } from "./GraphQLIntrospection";
|
|||
import { getStream } from "./get-stream/index";
|
||||
import { train } from "./MarkovChain";
|
||||
import { sourcesFromPostmanCollection } from "./PostmanCollection";
|
||||
import { readableFromFileOrURL, readFromFileOrURL, FetchingJSONSchemaStore } from "./NodeIO";
|
||||
|
||||
const commandLineArgs = require("command-line-args");
|
||||
const getUsage = require("command-line-usage");
|
||||
const chalk = require("chalk");
|
||||
const fetch = require("node-fetch");
|
||||
const wordWrap: (s: string) => string = require("wordwrap")(90);
|
||||
|
||||
const packageJSON = require("../package.json");
|
||||
|
@ -69,21 +65,11 @@ export interface CLIOptions {
|
|||
help: boolean;
|
||||
quiet: boolean;
|
||||
version: boolean;
|
||||
}
|
||||
|
||||
async function readableFromFileOrUrl(fileOrUrl: string): Promise<Readable> {
|
||||
if (isURL(fileOrUrl)) {
|
||||
const response = await fetch(fileOrUrl);
|
||||
return response.body;
|
||||
} else if (fs.existsSync(fileOrUrl)) {
|
||||
return fs.createReadStream(fileOrUrl);
|
||||
} else {
|
||||
return panic(`Input file ${fileOrUrl} does not exist`);
|
||||
}
|
||||
debug?: string;
|
||||
}
|
||||
|
||||
async function sourceFromFileOrUrlArray(name: string, filesOrUrls: string[]): Promise<JSONTypeSource> {
|
||||
const samples = await Promise.all(filesOrUrls.map(readableFromFileOrUrl));
|
||||
const samples = await Promise.all(filesOrUrls.map(readableFromFileOrURL));
|
||||
return { name, samples };
|
||||
}
|
||||
|
||||
|
@ -92,7 +78,7 @@ function typeNameFromFilename(filename: string): string {
|
|||
return name.substr(0, name.lastIndexOf("."));
|
||||
}
|
||||
|
||||
async function samplesFromDirectory(dataDir: string, schemaTopLevel: string | undefined): Promise<TypeSource[]> {
|
||||
async function samplesFromDirectory(dataDir: string, topLevelRefs: string[] | undefined): Promise<TypeSource[]> {
|
||||
async function readFilesOrURLsInDirectory(d: string): Promise<TypeSource[]> {
|
||||
const files = fs
|
||||
.readdirSync(d)
|
||||
|
@ -114,21 +100,23 @@ async function samplesFromDirectory(dataDir: string, schemaTopLevel: string | un
|
|||
}
|
||||
|
||||
if (file.endsWith(".url") || file.endsWith(".json")) {
|
||||
// FIXME: Why do we include the URI here?
|
||||
sourcesInDir.push({
|
||||
name,
|
||||
samples: [await readableFromFileOrUrl(fileOrUrl)]
|
||||
uri: fileOrUrl,
|
||||
samples: [await readableFromFileOrURL(fileOrUrl)]
|
||||
});
|
||||
} else if (file.endsWith(".schema")) {
|
||||
sourcesInDir.push({
|
||||
name,
|
||||
schema: await readableFromFileOrUrl(fileOrUrl),
|
||||
topLevelRefs: schemaTopLevel === undefined ? undefined : [schemaTopLevel]
|
||||
uri: fileOrUrl,
|
||||
topLevelRefs
|
||||
});
|
||||
} else if (file.endsWith(".gqlschema")) {
|
||||
assert(graphQLSchema === undefined, `More than one GraphQL schema in ${dataDir}`);
|
||||
graphQLSchema = await readableFromFileOrUrl(fileOrUrl);
|
||||
graphQLSchema = await readableFromFileOrURL(fileOrUrl);
|
||||
} else if (file.endsWith(".graphql")) {
|
||||
graphQLSources.push({ name, schema: undefined, query: await readableFromFileOrUrl(fileOrUrl) });
|
||||
graphQLSources.push({ name, schema: undefined, query: await readableFromFileOrURL(fileOrUrl) });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -254,7 +242,8 @@ function inferOptions(opts: Partial<CLIOptions>): CLIOptions {
|
|||
graphqlIntrospect: opts.graphqlIntrospect,
|
||||
graphqlServerHeader: opts.graphqlServerHeader,
|
||||
addSchemaTopLevel: opts.addSchemaTopLevel,
|
||||
template: opts.template
|
||||
template: opts.template,
|
||||
debug: opts.debug
|
||||
};
|
||||
/* tslint:enable */
|
||||
}
|
||||
|
@ -312,7 +301,7 @@ const optionDefinitions: OptionDefinition[] = [
|
|||
name: "add-schema-top-level",
|
||||
type: String,
|
||||
typeLabel: "REF",
|
||||
description: "Use JSON Schema definitions as top-levels. Must be `definitions/`."
|
||||
description: "Use JSON Schema definitions as top-levels. Must be `/definitions/`."
|
||||
},
|
||||
{
|
||||
name: "graphql-schema",
|
||||
|
@ -385,6 +374,12 @@ const optionDefinitions: OptionDefinition[] = [
|
|||
type: Boolean,
|
||||
description: "Don't show issues in the generated code."
|
||||
},
|
||||
{
|
||||
name: "debug",
|
||||
type: String,
|
||||
typeLabel: "OPTIONS",
|
||||
description: "Comma separated debug options: print-graph"
|
||||
},
|
||||
{
|
||||
name: "help",
|
||||
alias: "h",
|
||||
|
@ -515,38 +510,56 @@ function usage() {
|
|||
console.log(getUsage(sections));
|
||||
}
|
||||
|
||||
async function getSources(options: CLIOptions): Promise<TypeSource[]> {
|
||||
// Returns an array of [name, sourceURIs] pairs.
|
||||
async function getSourceURIs(options: CLIOptions): Promise<[string, string[]][]> {
|
||||
if (options.srcUrls !== undefined) {
|
||||
const json = JSON.parse(fs.readFileSync(options.srcUrls, "utf8"));
|
||||
const json = JSON.parse(await readFromFileOrURL(options.srcUrls));
|
||||
const jsonMap = urlsFromURLGrammar(json);
|
||||
const topLevels = Object.getOwnPropertyNames(jsonMap);
|
||||
return Promise.all(topLevels.map(name => sourceFromFileOrUrlArray(name, jsonMap[name])));
|
||||
return topLevels.map(name => [name, jsonMap[name]] as [string, string[]]);
|
||||
} else if (options.src.length === 0) {
|
||||
return [
|
||||
{
|
||||
name: options.topLevel,
|
||||
samples: [process.stdin]
|
||||
}
|
||||
];
|
||||
return [[options.topLevel, ["-"]]];
|
||||
} else {
|
||||
const exists = options.src.filter(fs.existsSync);
|
||||
const directories = exists.filter(x => fs.lstatSync(x).isDirectory());
|
||||
|
||||
let sources: TypeSource[] = [];
|
||||
for (const dataDir of directories) {
|
||||
sources = sources.concat(await samplesFromDirectory(dataDir, options.addSchemaTopLevel));
|
||||
}
|
||||
|
||||
// Every src that's not a directory is assumed to be a file or URL
|
||||
const filesOrUrls = options.src.filter(x => !_.includes(directories, x));
|
||||
if (!_.isEmpty(filesOrUrls)) {
|
||||
sources.push(await sourceFromFileOrUrlArray(options.topLevel, filesOrUrls));
|
||||
}
|
||||
|
||||
return sources;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function topLevelRefsForOptions(options: CLIOptions): string[] | undefined {
|
||||
return options.addSchemaTopLevel === undefined ? undefined : [options.addSchemaTopLevel];
|
||||
}
|
||||
|
||||
async function typeSourceForURIs(name: string, uris: string[], options: CLIOptions): Promise<TypeSource> {
|
||||
switch (options.srcLang) {
|
||||
case "json":
|
||||
return await sourceFromFileOrUrlArray(name, uris);
|
||||
case "schema":
|
||||
assert(uris.length === 1, `Must have exactly one schema for ${name}`);
|
||||
return {name, uri: uris[0], topLevelRefs: topLevelRefsForOptions(options) };
|
||||
default:
|
||||
return panic(`typeSourceForURIs must not be called for source language ${options.srcLang}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function getSources(options: CLIOptions): Promise<TypeSource[]> {
|
||||
const sourceURIs = await getSourceURIs(options);
|
||||
let sources: TypeSource[] = await Promise.all(sourceURIs.map(([name, uris]) => typeSourceForURIs(name, uris, options)));
|
||||
|
||||
const exists = options.src.filter(fs.existsSync);
|
||||
const directories = exists.filter(x => fs.lstatSync(x).isDirectory());
|
||||
|
||||
for (const dataDir of directories) {
|
||||
sources = sources.concat(await samplesFromDirectory(dataDir, topLevelRefsForOptions(options)));
|
||||
}
|
||||
|
||||
// Every src that's not a directory is assumed to be a file or URL
|
||||
const filesOrUrls = options.src.filter(x => !_.includes(directories, x));
|
||||
if (!_.isEmpty(filesOrUrls)) {
|
||||
sources.push(await typeSourceForURIs(options.topLevel, filesOrUrls, options));
|
||||
}
|
||||
|
||||
return sources;
|
||||
}
|
||||
|
||||
export async function main(args: string[] | Partial<CLIOptions>) {
|
||||
if (_.isArray(args) && args.length === 0) {
|
||||
usage();
|
||||
|
@ -604,13 +617,14 @@ export async function main(args: string[] | Partial<CLIOptions>) {
|
|||
schemaString = fs.readFileSync(schemaFile, "utf8");
|
||||
}
|
||||
const schema = JSON.parse(schemaString);
|
||||
const query = await readableFromFileOrUrl(queryFile);
|
||||
const query = await readableFromFileOrURL(queryFile);
|
||||
const name = numSources === 1 ? options.topLevel : typeNameFromFilename(queryFile);
|
||||
gqlSources.push({ name, schema, query });
|
||||
}
|
||||
sources = gqlSources;
|
||||
break;
|
||||
case "json":
|
||||
case "schema":
|
||||
sources = await getSources(options);
|
||||
break;
|
||||
case "postman-json":
|
||||
|
@ -628,25 +642,6 @@ export async function main(args: string[] | Partial<CLIOptions>) {
|
|||
}
|
||||
}
|
||||
break;
|
||||
case "schema":
|
||||
// Collect sources as JSON, then map to schema data
|
||||
for (const source of await getSources(options)) {
|
||||
if (isGraphQLSource(source)) {
|
||||
return panic("Cannot accept GraphQL for JSON Schema input");
|
||||
}
|
||||
if (isJSONSource(source)) {
|
||||
assert(source.samples.length === 1, `Please specify one schema file for ${source.name}`);
|
||||
sources.push({
|
||||
name: source.name,
|
||||
schema: source.samples[0],
|
||||
topLevelRefs:
|
||||
options.addSchemaTopLevel === undefined ? undefined : [options.addSchemaTopLevel]
|
||||
});
|
||||
} else {
|
||||
sources.push(source);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
panic(`Unsupported source language (${options.srcLang})`);
|
||||
break;
|
||||
|
@ -657,11 +652,12 @@ export async function main(args: string[] | Partial<CLIOptions>) {
|
|||
handlebarsTemplate = fs.readFileSync(options.template, "utf8");
|
||||
}
|
||||
|
||||
let findSimilarClassesSchema: string | undefined = undefined;
|
||||
if (options.findSimilarClassesSchema !== undefined) {
|
||||
findSimilarClassesSchema = fs.readFileSync(options.findSimilarClassesSchema, "utf8");
|
||||
let debugPrintGraph = false;
|
||||
if (options.debug !== undefined) {
|
||||
assert(options.debug === "print-graph", "The --debug option must be \"print-graph\"");
|
||||
debugPrintGraph = true;
|
||||
}
|
||||
|
||||
|
||||
let run = new Run({
|
||||
lang: options.lang,
|
||||
sources,
|
||||
|
@ -676,8 +672,10 @@ export async function main(args: string[] | Partial<CLIOptions>) {
|
|||
rendererOptions: options.rendererOptions,
|
||||
leadingComments,
|
||||
handlebarsTemplate,
|
||||
findSimilarClassesSchema,
|
||||
outputFilename: options.out !== undefined ? path.basename(options.out) : undefined
|
||||
findSimilarClassesSchemaURI: options.findSimilarClassesSchema,
|
||||
outputFilename: options.out !== undefined ? path.basename(options.out) : undefined,
|
||||
schemaStore: new FetchingJSONSchemaStore(),
|
||||
debugPrintGraph
|
||||
});
|
||||
|
||||
const resultsByFilename = await run.run();
|
||||
|
|
156
src/index.ts
156
src/index.ts
|
@ -2,14 +2,15 @@ import { getStream } from "./get-stream";
|
|||
import * as _ from "lodash";
|
||||
import { List, Map, OrderedMap, OrderedSet } from "immutable";
|
||||
import { Readable } from "stream";
|
||||
import * as URI from "urijs";
|
||||
|
||||
import * as targetLanguages from "./Language/All";
|
||||
import { TargetLanguage } from "./TargetLanguage";
|
||||
import { SerializedRenderResult, Annotation, Location, Span } from "./Source";
|
||||
import { assertNever, assert } from "./Support";
|
||||
import { assertNever, assert, panic, defined, forEachSync } from "./Support";
|
||||
import { CompressedJSON, Value } from "./CompressedJSON";
|
||||
import { combineClasses, findSimilarityCliques } from "./CombineClasses";
|
||||
import { addTypesInSchema, definitionRefsInSchema, Ref } from "./JSONSchemaInput";
|
||||
import { addTypesInSchema, Ref, JSONSchemaStore, definitionRefsInSchema, JSONSchema, checkJSONSchema } from "./JSONSchemaInput";
|
||||
import { TypeInference } from "./Inference";
|
||||
import { inferMaps } from "./InferMaps";
|
||||
import { TypeGraphBuilder } from "./TypeBuilder";
|
||||
|
@ -57,7 +58,8 @@ export function isJSONSource(source: TypeSource): source is JSONTypeSource {
|
|||
|
||||
export interface SchemaTypeSource {
|
||||
name: string;
|
||||
schema: StringInput;
|
||||
uri?: string;
|
||||
schema?: StringInput;
|
||||
topLevelRefs?: string[];
|
||||
}
|
||||
|
||||
|
@ -81,7 +83,7 @@ export interface Options {
|
|||
lang: string | TargetLanguage;
|
||||
sources: TypeSource[];
|
||||
handlebarsTemplate: string | undefined;
|
||||
findSimilarClassesSchema: string | undefined;
|
||||
findSimilarClassesSchemaURI: string | undefined;
|
||||
inferMaps: boolean;
|
||||
inferEnums: boolean;
|
||||
inferDates: boolean;
|
||||
|
@ -94,13 +96,15 @@ export interface Options {
|
|||
rendererOptions: RendererOptions;
|
||||
indentation: string | undefined;
|
||||
outputFilename: string;
|
||||
schemaStore: JSONSchemaStore | undefined;
|
||||
debugPrintGraph: boolean | undefined;
|
||||
}
|
||||
|
||||
const defaultOptions: Options = {
|
||||
lang: "ts",
|
||||
sources: [],
|
||||
handlebarsTemplate: undefined,
|
||||
findSimilarClassesSchema: undefined,
|
||||
findSimilarClassesSchemaURI: undefined,
|
||||
inferMaps: true,
|
||||
inferEnums: true,
|
||||
inferDates: true,
|
||||
|
@ -112,12 +116,14 @@ const defaultOptions: Options = {
|
|||
leadingComments: undefined,
|
||||
rendererOptions: {},
|
||||
indentation: undefined,
|
||||
outputFilename: "stdout"
|
||||
outputFilename: "stdout",
|
||||
schemaStore: undefined,
|
||||
debugPrintGraph: false
|
||||
};
|
||||
|
||||
type InputData = {
|
||||
samples: { [name: string]: { samples: Value[]; description?: string } };
|
||||
schemas: { [name: string]: { schema: any; topLevelRefs: string[] | undefined } };
|
||||
schemas: { [name: string]: { ref: Ref } };
|
||||
graphQLs: { [name: string]: { schema: any; query: string } };
|
||||
};
|
||||
|
||||
|
@ -129,10 +135,28 @@ async function toString(source: string | Readable): Promise<string> {
|
|||
return _.isString(source) ? source : await getStream(source);
|
||||
}
|
||||
|
||||
class InputJSONSchemaStore extends JSONSchemaStore {
|
||||
constructor(private readonly _inputs: Map<string, StringInput>, private readonly _delegate?: JSONSchemaStore) {
|
||||
super();
|
||||
}
|
||||
|
||||
async fetch(address: string): Promise<JSONSchema | undefined> {
|
||||
const maybeInput = this._inputs.get(address);
|
||||
if (maybeInput !== undefined) {
|
||||
return checkJSONSchema(JSON.parse(await toString(maybeInput)));
|
||||
}
|
||||
if (this._delegate === undefined) {
|
||||
return panic(`Schema URI ${address} requested, but no store given`);
|
||||
}
|
||||
return await this._delegate.fetch(address);
|
||||
}
|
||||
}
|
||||
|
||||
export class Run {
|
||||
private _compressedJSON: CompressedJSON;
|
||||
private _allInputs: InputData;
|
||||
private _options: Options;
|
||||
private _schemaStore: JSONSchemaStore | undefined;
|
||||
|
||||
constructor(options: Partial<Options>) {
|
||||
this._options = _.mergeWith(_.clone(options), defaultOptions, (o, s) => (o === undefined ? s : o));
|
||||
|
@ -145,7 +169,11 @@ export class Run {
|
|||
this._compressedJSON = new CompressedJSON(makeDate, makeTime, makeDateTime);
|
||||
}
|
||||
|
||||
private makeGraph = (): TypeGraph => {
|
||||
private getSchemaStore(): JSONSchemaStore {
|
||||
return defined(this._schemaStore);
|
||||
}
|
||||
|
||||
private async makeGraph(): Promise<TypeGraph> {
|
||||
const targetLanguage = getTargetLanguage(this._options.lang);
|
||||
const stringTypeMapping = targetLanguage.stringTypeMapping;
|
||||
const conflateNumbers = !targetLanguage.supportsUnionsWithBothNumberTypes;
|
||||
|
@ -158,27 +186,14 @@ export class Run {
|
|||
false
|
||||
);
|
||||
|
||||
if (this._options.findSimilarClassesSchema !== undefined) {
|
||||
const schema = JSON.parse(this._options.findSimilarClassesSchema);
|
||||
const name = "ComparisonBaseRoot";
|
||||
addTypesInSchema(typeBuilder, schema, Map([[name, Ref.root] as [string, Ref]]));
|
||||
}
|
||||
|
||||
// JSON Schema
|
||||
Map(this._allInputs.schemas).forEach(({ schema, topLevelRefs }, name) => {
|
||||
let references: Map<string, Ref>;
|
||||
if (topLevelRefs === undefined) {
|
||||
references = Map([[name, Ref.root] as [string, Ref]]);
|
||||
} else {
|
||||
assert(
|
||||
topLevelRefs.length === 1 && topLevelRefs[0] === "definitions/",
|
||||
"Schema top level refs must be `definitions/`"
|
||||
);
|
||||
references = definitionRefsInSchema(schema);
|
||||
assert(references.size > 0, "No definitions in JSON Schema");
|
||||
}
|
||||
addTypesInSchema(typeBuilder, schema, references);
|
||||
});
|
||||
let schemaInputs = Map(this._allInputs.schemas).map(({ref}) => ref);
|
||||
if (this._options.findSimilarClassesSchemaURI !== undefined) {
|
||||
schemaInputs = schemaInputs.set("ComparisonBaseRoot", Ref.parse(this._options.findSimilarClassesSchemaURI));
|
||||
}
|
||||
if (!schemaInputs.isEmpty()) {
|
||||
await addTypesInSchema(typeBuilder, this.getSchemaStore(), schemaInputs);
|
||||
}
|
||||
|
||||
// GraphQL
|
||||
const numInputs = Object.keys(this._allInputs.graphQLs).length;
|
||||
|
@ -211,6 +226,10 @@ export class Run {
|
|||
}
|
||||
|
||||
let graph = typeBuilder.finish();
|
||||
if (this._options.debugPrintGraph) {
|
||||
graph.setPrintOnRewrite();
|
||||
graph.printGraph();
|
||||
}
|
||||
|
||||
if (haveSchemas) {
|
||||
let intersectionsDone = false;
|
||||
|
@ -230,7 +249,7 @@ export class Run {
|
|||
} while (!intersectionsDone || !unionsDone);
|
||||
}
|
||||
|
||||
if (this._options.findSimilarClassesSchema !== undefined) {
|
||||
if (this._options.findSimilarClassesSchemaURI !== undefined) {
|
||||
return graph;
|
||||
}
|
||||
|
||||
|
@ -258,11 +277,13 @@ export class Run {
|
|||
graph = graph.garbageCollect(this._options.alphabetizeProperties);
|
||||
|
||||
gatherNames(graph);
|
||||
|
||||
// graph.printGraph();
|
||||
if (this._options.debugPrintGraph) {
|
||||
console.log("\n# gather names");
|
||||
graph.printGraph();
|
||||
}
|
||||
|
||||
return graph;
|
||||
};
|
||||
}
|
||||
|
||||
private makeSimpleTextResult(lines: string[]): OrderedMap<string, SerializedRenderResult> {
|
||||
return OrderedMap([[this._options.outputFilename, { lines, annotations: List() }]] as [
|
||||
|
@ -271,9 +292,67 @@ export class Run {
|
|||
][]);
|
||||
}
|
||||
|
||||
private addSchemaInput(name: string, ref: Ref): void {
|
||||
if (_.has(this._allInputs.schemas, name)) {
|
||||
throw new Error(`More than one schema given for ${name}`);
|
||||
}
|
||||
|
||||
this._allInputs.schemas[name] = { ref };
|
||||
}
|
||||
|
||||
public run = async (): Promise<OrderedMap<string, SerializedRenderResult>> => {
|
||||
const targetLanguage = getTargetLanguage(this._options.lang);
|
||||
|
||||
let schemaInputs: Map<string, StringInput> = Map();
|
||||
let schemaSources: List<[uri.URI, SchemaTypeSource]> = List();
|
||||
for (const source of this._options.sources) {
|
||||
if (!isSchemaSource(source)) continue;
|
||||
const { uri, schema } = source;
|
||||
|
||||
let normalizedURI: uri.URI;
|
||||
if (uri === undefined) {
|
||||
normalizedURI = new URI(`-${schemaInputs.size + 1}`);
|
||||
} else {
|
||||
normalizedURI = new URI(uri).normalize();
|
||||
}
|
||||
|
||||
if (schema === undefined) {
|
||||
assert(uri !== undefined, "URI must be given if schema source is not specified");
|
||||
} else {
|
||||
schemaInputs = schemaInputs.set(normalizedURI.clone().hash("").toString(), schema);
|
||||
}
|
||||
|
||||
schemaSources = schemaSources.push([normalizedURI, source]);
|
||||
}
|
||||
|
||||
if (!schemaSources.isEmpty()) {
|
||||
if (schemaInputs.isEmpty()) {
|
||||
if (this._options.schemaStore === undefined) {
|
||||
return panic("Must have a schema store to process JSON Schema");
|
||||
}
|
||||
this._schemaStore = this._options.schemaStore;
|
||||
} else {
|
||||
this._schemaStore = new InputJSONSchemaStore(schemaInputs, this._options.schemaStore);
|
||||
}
|
||||
|
||||
await forEachSync(schemaSources, async ([normalizedURI, source]) => {
|
||||
const { name, topLevelRefs } = source;
|
||||
|
||||
if (topLevelRefs !== undefined) {
|
||||
assert(
|
||||
topLevelRefs.length === 1 && topLevelRefs[0] === "/definitions/",
|
||||
"Schema top level refs must be `/definitions/`"
|
||||
);
|
||||
const definitionRefs = await definitionRefsInSchema(this.getSchemaStore(), normalizedURI.toString());
|
||||
definitionRefs.forEach((ref, name) => {
|
||||
this.addSchemaInput(name, ref);
|
||||
});
|
||||
} else {
|
||||
this.addSchemaInput(name, Ref.parse(normalizedURI.toString()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const source of this._options.sources) {
|
||||
if (isGraphQLSource(source)) {
|
||||
const { name, schema, query } = source;
|
||||
|
@ -290,25 +369,18 @@ export class Run {
|
|||
this._allInputs.samples[name].description = description;
|
||||
}
|
||||
}
|
||||
} else if (isSchemaSource(source)) {
|
||||
const { name, schema, topLevelRefs } = source;
|
||||
const input = JSON.parse(await toString(schema));
|
||||
if (_.has(this._allInputs.schemas, name)) {
|
||||
throw new Error(`More than one schema given for ${name}`);
|
||||
}
|
||||
this._allInputs.schemas[name] = { schema: input, topLevelRefs };
|
||||
} else {
|
||||
} else if (!isSchemaSource(source)) {
|
||||
assertNever(source);
|
||||
}
|
||||
}
|
||||
|
||||
const graph = this.makeGraph();
|
||||
const graph = await this.makeGraph();
|
||||
|
||||
if (this._options.noRender) {
|
||||
return this.makeSimpleTextResult(["Done.", ""]);
|
||||
}
|
||||
|
||||
if (this._options.findSimilarClassesSchema !== undefined) {
|
||||
if (this._options.findSimilarClassesSchemaURI !== undefined) {
|
||||
const cliques = findSimilarityCliques(graph, true);
|
||||
const lines: string[] = [];
|
||||
if (cliques.length === 0) {
|
||||
|
|
|
@ -2,7 +2,10 @@
|
|||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"lib": ["es2015", "esnext.asynciterable"],
|
||||
"lib": [
|
||||
"es2015",
|
||||
"esnext.asynciterable"
|
||||
],
|
||||
"allowJs": false,
|
||||
"declaration": true,
|
||||
"typeRoots": ["../node_modules/@types"],
|
||||
|
@ -11,4 +14,4 @@
|
|||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -38,23 +38,23 @@ function pathWithoutExtension(fullPath: string, extension: string): string {
|
|||
return path.join(path.dirname(fullPath), path.basename(fullPath, extension));
|
||||
}
|
||||
|
||||
function jsonTestFiles(base: string): string[] {
|
||||
const jsonFiles: string[] = [];
|
||||
let fn = `${base}.json`;
|
||||
function additionalTestFiles(base: string, extension: string): string[] {
|
||||
const additionalFiles: string[] = [];
|
||||
let fn = `${base}.${extension}`;
|
||||
if (fs.existsSync(fn)) {
|
||||
jsonFiles.push(fn);
|
||||
additionalFiles.push(fn);
|
||||
}
|
||||
let i = 1;
|
||||
for (;;) {
|
||||
fn = `${base}.${i.toString()}.json`;
|
||||
fn = `${base}.${i.toString()}.${extension}`;
|
||||
if (fs.existsSync(fn)) {
|
||||
jsonFiles.push(fn);
|
||||
additionalFiles.push(fn);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return jsonFiles;
|
||||
return additionalFiles;
|
||||
}
|
||||
|
||||
export abstract class Fixture {
|
||||
|
@ -373,19 +373,22 @@ class JSONSchemaFixture extends LanguageFixture {
|
|||
}
|
||||
|
||||
additionalFiles(sample: Sample): string[] {
|
||||
return jsonTestFiles(pathWithoutExtension(sample.path, ".schema"));
|
||||
const baseName = pathWithoutExtension(sample.path, ".schema");
|
||||
return additionalTestFiles(baseName, "json").concat(additionalTestFiles(baseName, "ref"));
|
||||
}
|
||||
|
||||
async test(
|
||||
_sample: string,
|
||||
_additionalRendererOptions: RendererOptions,
|
||||
jsonFiles: string[]
|
||||
additionalFiles: string[]
|
||||
): Promise<void> {
|
||||
if (this.language.compileCommand) {
|
||||
await execAsync(this.language.compileCommand);
|
||||
}
|
||||
for (const json of jsonFiles) {
|
||||
const jsonBase = path.basename(json);
|
||||
for (const filename of additionalFiles) {
|
||||
if (!filename.endsWith(".json")) continue;
|
||||
|
||||
const jsonBase = path.basename(filename);
|
||||
compareJsonFileToJson({
|
||||
expectedFile: jsonBase,
|
||||
given: { command: this.language.runCommand(jsonBase) },
|
||||
|
@ -441,7 +444,7 @@ class GraphQLFixture extends LanguageFixture {
|
|||
|
||||
additionalFiles(sample: Sample): string[] {
|
||||
const baseName = pathWithoutExtension(sample.path, ".graphql");
|
||||
return jsonTestFiles(baseName).concat(graphQLSchemaFilename(baseName));
|
||||
return additionalTestFiles(baseName, "json").concat(graphQLSchemaFilename(baseName));
|
||||
}
|
||||
|
||||
async test(
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"foo": 123
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"foo": {
|
||||
"type": [
|
||||
"integer"
|
||||
],
|
||||
"enum": [
|
||||
1,
|
||||
2,
|
||||
true,
|
||||
"a",
|
||||
"b",
|
||||
"c"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"foo"
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{ "item": 123 }
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"$id": "http://example.net/root.json",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"item": { "$ref": "#item" }
|
||||
},
|
||||
"required": ["item"],
|
||||
"definitions": {
|
||||
"single": {
|
||||
"$id": "#item",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"bar": 123
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"$id": "http://foo/root.json",
|
||||
"$ref": "#/definitions/foo",
|
||||
"definitions": {
|
||||
"foo": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bar": {
|
||||
"$ref": "http://foo/root.json#/definitions/bar"
|
||||
}
|
||||
},
|
||||
"required": ["bar"]
|
||||
},
|
||||
"bar": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,12 +6,14 @@
|
|||
"required": ["foo"],
|
||||
"definitions": {
|
||||
"base1": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bar": { "type": "boolean" }
|
||||
},
|
||||
"required": ["bar"]
|
||||
},
|
||||
"base2": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"quux": { "type": "string" }
|
||||
},
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
[1, 2, 3]
|
|
@ -0,0 +1 @@
|
|||
{ "foo": 123 }
|
|
@ -0,0 +1 @@
|
|||
"abc"
|
|
@ -0,0 +1 @@
|
|||
true
|
|
@ -0,0 +1 @@
|
|||
3.141592
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"properties": { "foo": { "type": "integer" } },
|
||||
"required": ["foo"],
|
||||
"items": { "type": "integer" }
|
||||
}
|
|
@ -6,12 +6,14 @@
|
|||
"required": ["foo"],
|
||||
"definitions": {
|
||||
"base1": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"bar": { "type": "boolean" }
|
||||
},
|
||||
"required": ["bar"]
|
||||
},
|
||||
"base2": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"quux": { "type": "string" }
|
||||
},
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"foo": {
|
||||
"type": "integer"
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"foo": 123,
|
||||
"bar": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
]
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"foo": { "type": "integer" }
|
||||
},
|
||||
"required": ["foo", "bar"]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"$ref": "#/definitions/List",
|
||||
"definitions": {
|
||||
"List": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"next": {
|
||||
"$ref": "#/definitions/List"
|
||||
}
|
||||
},
|
||||
"title": "List",
|
||||
"description": "A recursive class type"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{"next": {}}
|
|
@ -0,0 +1 @@
|
|||
{"next": {"next": {}}}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"$ref": "simple-ref.1.ref#/definitions/List"
|
||||
}
|
|
@ -9,24 +9,28 @@
|
|||
"$ref": "#/definitions/UnionList"
|
||||
}
|
||||
},
|
||||
"required": ["list"]
|
||||
"required": [
|
||||
"list"
|
||||
]
|
||||
},
|
||||
"UnionList": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"next": {
|
||||
"$ref": "#/definitions/UnionList"
|
||||
}
|
||||
},
|
||||
"required": ["next"]
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
}
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"next": {
|
||||
"$ref": "#/definitions/UnionList"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"next"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -103,7 +103,10 @@ export const RubyLanguage: Language = {
|
|||
output: "TopLevel.rb",
|
||||
topLevel: "TopLevel",
|
||||
skipJSON: [],
|
||||
skipSchema: [],
|
||||
skipSchema: [
|
||||
// FIXME: I don't know what the issue is here
|
||||
"implicit-class-array-union.schema"
|
||||
],
|
||||
skipMiscJSON: false,
|
||||
rendererOptions: {},
|
||||
quickTestRendererOptions: [],
|
||||
|
@ -185,6 +188,7 @@ export const ElmLanguage: Language = {
|
|||
"mutually-recursive.schema", // recursion
|
||||
"postman-collection.schema", // recursion
|
||||
"vega-lite.schema", // recursion
|
||||
"simple-ref.schema", // recursion
|
||||
"keyword-unions.schema" // can't handle "hasOwnProperty" for some reason
|
||||
],
|
||||
rendererOptions: {},
|
||||
|
@ -212,7 +216,10 @@ export const SwiftLanguage: Language = {
|
|||
"nst-test-suite.json"
|
||||
],
|
||||
skipMiscJSON: false,
|
||||
skipSchema: [],
|
||||
skipSchema: [
|
||||
// The top-level is a union
|
||||
"implicit-class-array-union.schema"
|
||||
],
|
||||
rendererOptions: {},
|
||||
quickTestRendererOptions: [{ "struct-or-class": "class" }, { density: "dense" }, { density: "normal" }],
|
||||
sourceFiles: ["src/Language/Swift.ts"]
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
"no-empty": [true, "allow-empty-catch"],
|
||||
"no-eval": true,
|
||||
"no-shadowed-variable": true,
|
||||
"no-string-literal": true,
|
||||
"no-string-literal": false,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-trailing-whitespace": false,
|
||||
"no-unsafe-any": false,
|
||||
|
|
Загрузка…
Ссылка в новой задаче