From b7387527bd78f2e935c3252ab7e730712cff4e5c Mon Sep 17 00:00:00 2001 From: Mark Probst Date: Tue, 14 Aug 2018 07:43:09 -0700 Subject: [PATCH] Allow languages to define their own date/time formats --- src/quicktype-core/DateTime.ts | 53 +++++++++++++--------- src/quicktype-core/StringTypes.ts | 13 ++++-- src/quicktype-core/TargetLanguage.ts | 5 ++ src/quicktype-core/input/CompressedJSON.ts | 11 +++-- src/quicktype-core/input/Inputs.ts | 13 ++++-- test/fixtures.ts | 6 ++- 6 files changed, 62 insertions(+), 39 deletions(-) diff --git a/src/quicktype-core/DateTime.ts b/src/quicktype-core/DateTime.ts index de43d6b0..20d0340b 100644 --- a/src/quicktype-core/DateTime.ts +++ b/src/quicktype-core/DateTime.ts @@ -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]); + } } diff --git a/src/quicktype-core/StringTypes.ts b/src/quicktype-core/StringTypes.ts index 415a26c6..0dc66be0 100644 --- a/src/quicktype-core/StringTypes.ts +++ b/src/quicktype-core/StringTypes.ts @@ -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"; diff --git a/src/quicktype-core/TargetLanguage.ts b/src/quicktype-core/TargetLanguage.ts index ec0985aa..b6a5d093 100644 --- a/src/quicktype-core/TargetLanguage.ts +++ b/src/quicktype-core/TargetLanguage.ts @@ -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(); + } } diff --git a/src/quicktype-core/input/CompressedJSON.ts b/src/quicktype-core/input/CompressedJSON.ts index 0a97511b..ca0dc892 100644 --- a/src/quicktype-core/input/CompressedJSON.ts +++ b/src/quicktype-core/input/CompressedJSON.ts @@ -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 { 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((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(); diff --git a/src/quicktype-core/input/Inputs.ts b/src/quicktype-core/input/Inputs.ts index afa63d5f..772a3a3a 100644 --- a/src/quicktype-core/input/Inputs.ts +++ b/src/quicktype-core/input/Inputs.ts @@ -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 { readonly kind: string; @@ -113,12 +114,14 @@ export class JSONInput implements Input { } } -// 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); } diff --git a/test/fixtures.ts b/test/fixtures.ts index ed453217..db176c30 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -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.", {