Allow languages to define their own date/time formats

This commit is contained in:
Mark Probst 2018-08-14 07:43:09 -07:00
Родитель 608fa3978c
Коммит b7387527bd
6 изменённых файлов: 62 добавлений и 39 удалений

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

@ -28,29 +28,38 @@ const DATE = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
const DAYS = [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
const TIME = /^(\d\d):(\d\d):(\d\d)(\.\d+)?(z|[+-]\d\d:\d\d)?$/i;
export function isDate(str: string) {
// full-date from http://tools.ietf.org/html/rfc3339#section-5.6
const matches = str.match(DATE);
if (matches === null) return false;
const month = +matches[2];
const day = +matches[3];
return month >= 1 && month <= 12 && day >= 1 && day <= DAYS[month];
}
export function isTime(str: string): boolean {
const matches = str.match(TIME);
if (matches === null) return false;
const hour = +matches[1];
const minute = +matches[2];
const second = +matches[3];
return hour <= 23 && minute <= 59 && second <= 59;
export interface DateTimeRecognizer {
isDate(s: string): boolean;
isTime(s: string): boolean;
isDateTime(s: string): boolean;
}
const DATE_TIME_SEPARATOR = /t|\s/i;
export function isDateTime(str: string): boolean {
// http://tools.ietf.org/html/rfc3339#section-5.6
const dateTime = str.split(DATE_TIME_SEPARATOR);
return dateTime.length === 2 && isDate(dateTime[0]) && isTime(dateTime[1]);
export class DefaultDateTimeRecognizer implements DateTimeRecognizer {
isDate(str: string) {
// full-date from http://tools.ietf.org/html/rfc3339#section-5.6
const matches = str.match(DATE);
if (matches === null) return false;
const month = +matches[2];
const day = +matches[3];
return month >= 1 && month <= 12 && day >= 1 && day <= DAYS[month];
}
isTime(str: string): boolean {
const matches = str.match(TIME);
if (matches === null) return false;
const hour = +matches[1];
const minute = +matches[2];
const second = +matches[3];
return hour <= 23 && minute <= 59 && second <= 59;
}
isDateTime(str: string): boolean {
// http://tools.ietf.org/html/rfc3339#section-5.6
const dateTime = str.split(DATE_TIME_SEPARATOR);
return dateTime.length === 2 && this.isDate(dateTime[0]) && this.isTime(dateTime[1]);
}
}

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

@ -14,7 +14,7 @@ import { TypeAttributeKind } from "./TypeAttributes";
import { defined, assert } from "./support/Support";
import { StringTypeMapping, stringTypeMappingGet } from "./TypeBuilder";
import { TransformedStringTypeKind } from "./Type";
import { isDate, isTime, isDateTime } from "./DateTime";
import { DateTimeRecognizer } from "./DateTime";
export class StringTypes {
static readonly unrestricted: StringTypes = new StringTypes(undefined, new Set());
@ -190,14 +190,17 @@ function isUUID(s: string): boolean {
*
* @param s The string for which to determine the transformed string type kind.
*/
export function inferTransformedStringTypeKindForString(s: string): TransformedStringTypeKind | undefined {
export function inferTransformedStringTypeKindForString(
s: string,
recognizer: DateTimeRecognizer
): TransformedStringTypeKind | undefined {
if (s.length === 0 || "0123456789-abcdeft".indexOf(s[0]) < 0) return undefined;
if (isDate(s)) {
if (recognizer.isDate(s)) {
return "date";
} else if (isTime(s)) {
} else if (recognizer.isTime(s)) {
return "time";
} else if (isDateTime(s)) {
} else if (recognizer.isDateTime(s)) {
return "date-time";
} else if (isIntegerString(s)) {
return "integer-string";

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

@ -8,6 +8,7 @@ import { StringTypeMapping } from "./TypeBuilder";
import { defined } from "./support/Support";
import { ConvenienceRenderer } from "./ConvenienceRenderer";
import { Type } from "./Type";
import { DateTimeRecognizer, DefaultDateTimeRecognizer } from "./DateTime";
export abstract class TargetLanguage {
constructor(readonly displayName: string, readonly names: string[], readonly extension: string) {}
@ -77,4 +78,8 @@ export abstract class TargetLanguage {
needsTransformerForType(_t: Type): boolean {
return false;
}
get dateTimeRecognizer(): DateTimeRecognizer {
return new DefaultDateTimeRecognizer();
}
}

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

@ -5,6 +5,7 @@ import { addHashCode, hashCodeInit, hashString } from "collection-utils";
import { defined, panic, assert } from "../support/Support";
import { inferTransformedStringTypeKindForString } from "../StringTypes";
import { TransformedStringTypeKind, isPrimitiveStringTypeKind } from "../Type";
import { DateTimeRecognizer } from "../DateTime";
const Combo = require("stream-json/Combo");
@ -72,13 +73,13 @@ export class CompressedJSON {
private _objects: Value[][] = [];
private _arrays: Value[][] = [];
[key: string]: any;
constructor(private readonly _dateTimeRecognizer: DateTimeRecognizer) {}
async readFromStream(readStream: stream.Readable): Promise<Value> {
const combo = new Combo({ packKeys: true, packStrings: true });
combo.on("data", (item: { name: string; value: string | undefined }) => {
if (typeof methodMap[item.name] === "string") {
this[methodMap[item.name]](item.value);
(this as any)[methodMap[item.name]](item.value);
}
});
const promise = new Promise<Value>((resolve, reject) => {
@ -223,9 +224,9 @@ export class CompressedJSON {
defined(this._ctx).currentKey = s;
};
protected handleStringValue = (s: string): void => {
protected handleStringValue(s: string): void {
let value: Value | undefined = undefined;
const format = inferTransformedStringTypeKindForString(s);
const format = inferTransformedStringTypeKindForString(s, this._dateTimeRecognizer);
if (format !== undefined) {
value = makeValue(Tag.TransformedString, this.internString(format));
} else if (s.length <= 64) {
@ -234,7 +235,7 @@ export class CompressedJSON {
value = makeValue(Tag.UninternedString, 0);
}
this.commitValue(value);
};
}
protected handleStartNumber = (): void => {
this.pushContext();

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

@ -1,7 +1,7 @@
import { iterableFirst, iterableFind, iterableSome, setFilterMap, withDefault } from "collection-utils";
import { Value, CompressedJSON } from "./CompressedJSON";
import { panic, errorMessage, toReadable, StringInput } from "../support/Support";
import { panic, errorMessage, toReadable, StringInput, defined } from "../support/Support";
import { messageError } from "../Messages";
import { TypeBuilder } from "../TypeBuilder";
import { makeNamesTypeAttributes } from "../TypeNames";
@ -9,6 +9,7 @@ import { descriptionTypeAttributeKind } from "../Description";
import { TypeInference } from "./Inference";
import { TargetLanguage } from "../TargetLanguage";
import { RunContext } from "../Run";
import { languageNamed } from "../language/All";
export interface Input<T> {
readonly kind: string;
@ -113,12 +114,14 @@ export class JSONInput implements Input<JSONSourceData> {
}
}
// FIXME: Remove this in the next major API.
export function jsonInputForTargetLanguage(
_targetLanguage: string | TargetLanguage,
_languages?: TargetLanguage[]
targetLanguage: string | TargetLanguage,
languages?: TargetLanguage[]
): JSONInput {
const compressedJSON = new CompressedJSON();
if (typeof targetLanguage === "string") {
targetLanguage = defined(languageNamed(targetLanguage, languages));
}
const compressedJSON = new CompressedJSON(targetLanguage.dateTimeRecognizer);
return new JSONInput(compressedJSON);
}

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

@ -25,7 +25,7 @@ import {
import * as languages from "./languages";
import { RendererOptions } from "../dist/quicktype-core/Run";
import { mustNotHappen, defined } from "../dist/quicktype-core/support/Support";
import { isDateTime } from "../dist/quicktype-core/DateTime";
import { DefaultDateTimeRecognizer } from "../dist/quicktype-core/DateTime";
const chalk = require("chalk");
const timeout = require("promise-timeout").timeout;
@ -399,6 +399,8 @@ class JSONToXToYFixture extends JSONFixture {
}
}
const dateTimeRecognizer = new DefaultDateTimeRecognizer();
// This tests generating Schema from JSON, and then generating
// target code from that Schema. The target code is then run on
// the original JSON. Also generating a Schema from the Schema
@ -426,7 +428,7 @@ class JSONSchemaJSONFixture extends JSONToXToYFixture {
// JSON formats that we use for transformed type kinds must be registered here
// with a validation function.
// FIXME: Unify this with what's in StringTypes.ts.
ajv.addFormat("date-time", isDateTime);
ajv.addFormat("date-time", (s: string) => dateTimeRecognizer.isDateTime(s));
let valid = ajv.validate(schema, input);
if (!valid) {
failWith("Generated schema does not validate input JSON.", {