Allow languages to define their own date/time formats
This commit is contained in:
Родитель
608fa3978c
Коммит
b7387527bd
|
@ -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.", {
|
||||
|
|
Загрузка…
Ссылка в новой задаче