Merge pull request #654 from quicktype/foreign-schema-refs

Foreign schema refs
This commit is contained in:
Mark Probst 2018-03-16 17:15:26 -07:00 коммит произвёл GitHub
Родитель 26d1eef068 255becf2f9
Коммит 946467a718
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
48 изменённых файлов: 1490 добавлений и 457 удалений

21
lib/@types/urijs/LICENSE Normal file
Просмотреть файл

@ -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>.

263
lib/@types/urijs/index.d.ts поставляемый Normal file
Просмотреть файл

@ -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];
})
);
}

34
src/NodeIO.ts Normal file
Просмотреть файл

@ -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);

Просмотреть файл

@ -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();

Просмотреть файл

@ -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,