Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1542,7 +1542,7 @@ export class CPlusPlusRenderer extends ConvenienceRenderer {
key,
") != j.end() && !std::regex_match(j.at(",
key,
').get<std::string>(), std::regex("^[0-9]{4}-[0-9]{2}-[0-9]{2}T.*$"))) throw std::runtime_error("Expected date-time");',
').get<std::string>(), std::regex("^(?:(?:[0-9]{4}-(?:0[13578]|1[02])-(?:0[1-9]|[12][0-9]|3[01]))|(?:[0-9]{4}-(?:0[469]|11)-(?:0[1-9]|[12][0-9]|30))|(?:[0-9]{4}-02-(?:0[1-9]|1[0-9]|2[0-8]))|(?:(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:[02468][048]|[13579][26])00)-02-29))[Tt](?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:[.][0-9]+)?(?:[Zz]|[+-](?:[01][0-9]|2[0-3]):[0-5][0-9])?$"))) throw std::runtime_error("Expected date-time");',
);
}

Expand Down
15 changes: 13 additions & 2 deletions packages/quicktype-core/src/language/Dart/DartRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export class DartRenderer extends ConvenienceRenderer {
>();

private _needEnumValues = false;
private _needDateTimeParser = false;

private classCounter = 0;

Expand Down Expand Up @@ -539,20 +540,21 @@ export class DartRenderer extends ConvenienceRenderer {
];
case "date-time":
case "date":
this._needDateTimeParser = true;
if (
(transformedStringType.isNullable || isNullable) &&
!this._options.requiredProperties
) {
return [
dynamic,
" == null ? null : ",
"DateTime.parse(",
"_parseDateTime(",
dynamic,
")",
];
}

return ["DateTime.parse(", dynamic, ")"];
return ["_parseDateTime(", dynamic, ")"];
default:
return dynamic;
}
Expand Down Expand Up @@ -1065,5 +1067,14 @@ export class DartRenderer extends ConvenienceRenderer {
if (this._needEnumValues) {
this.emitEnumValues();
}
if (this._needDateTimeParser)
this.emitMultiline(`
DateTime _parseDateTime(String value) {
final date = value.substring(0, 10);
if (!DateTime.parse(date + 'T00:00:00Z').toIso8601String().startsWith(date)) {
throw FormatException('Invalid date-time', value);
}
return DateTime.parse(value.toUpperCase());
}`);
}
}
12 changes: 9 additions & 3 deletions packages/quicktype-core/src/language/Java/DateTimeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export abstract class JavaDateTimeProvider {
public constructor(
protected readonly _renderer: JavaRenderer,
protected readonly _className: string,
protected readonly _useStrictDateTime = false,
) {}

public abstract keywords: string[];
Expand Down Expand Up @@ -105,15 +106,20 @@ export class Java8DateTimeProvider extends JavaDateTimeProvider {
".appendOptional(DateTimeFormatter.ISO_INSTANT)",
);
this._renderer.emitLine(
'.appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SX"))',
'.appendOptional(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss.SX"))',
);
this._renderer.emitLine(
'.appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ssX"))',
'.appendOptional(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ssX"))',
);
this._renderer.emitLine(
'.appendOptional(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))',
'.appendOptional(DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss"))',
);
this._renderer.emitLine(".toFormatter()");
if (this._useStrictDateTime) {
this._renderer.emitLine(
".withResolverStyle(java.time.format.ResolverStyle.STRICT)",
);
}
this._renderer.emitLine(".withZone(ZoneOffset.UTC);");
}),
);
Expand Down
3 changes: 3 additions & 0 deletions packages/quicktype-core/src/language/Java/JavaRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ export class JavaRenderer extends ConvenienceRenderer {
this._dateTimeProvider = new Java8DateTimeProvider(
this,
this._converterClassname,
[...renderContext.typeGraph.allTypesUnordered()].some(
(t) => t.kind === "date-time",
),
);
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -445,10 +445,15 @@ ${hasArrayConstraints ? ' if ((typ.min !== undefined && val.length < typ.
if (val === null) {
return null;
}
if (!(val instanceof Date) && (typeof val !== "string" || !/^[0-9]{4}-(?:0[1-9]|1[0-2])-(?:[0-2][0-9]|3[01])(?:T(?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:[.][0-9]+)?(?:Z|[+-](?:[01][0-9]|2[0-3]):[0-5][0-9]))?$/i.test(val)))
return invalidValue(l("Date"), val, key, parent);
const d = new Date(val);
if (isNaN(d.valueOf())) {
return invalidValue(l("Date"), val, key, parent);
}
const date = typeof val === "string" ? val.slice(0, 10) : null;
if (date !== null && new Date(date + "T00:00:00Z").toISOString().slice(0, 10) !== date)
return invalidValue(l("Date"), val, key, parent);
return d;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ export class JavaScriptPropTypesRenderer extends ConvenienceRenderer {
return '(props, name) => props[name] == null || /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i.test(props[name]) ? null : new Error("Expected UUID")';
}
if (transformedStringType.kind === "date-time") {
return '(props, name) => props[name] == null || typeof props[name] === "string" && !Number.isNaN(Date.parse(props[name])) ? null : new Error("Expected date-time")';
return '(props, name) => props[name] == null || typeof props[name] === "string" && /^(\\d{4}-(?:0[1-9]|1[0-2])-(?:[0-2]\\d|3[01]))(?:T(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z|[+-](?:[01]\\d|2[0-3]):[0-5]\\d))?$/i.test(props[name]) && !Number.isNaN(Date.parse(`${props[name].slice(0, 10)}T00:00:00Z`)) && new Date(Date.parse(`${props[name].slice(0, 10)}T00:00:00Z`)).toISOString().slice(0, 10) === props[name].slice(0, 10) ? null : new Error("Expected date-time")';
}
return "PropTypes.string";
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -936,7 +936,10 @@ export class ObjectiveCRenderer extends ConvenienceRenderer {
}
if (property.type.kind === "date-time") {
this.emitLine(
`if (dict[@"${objectiveCStringEscape(jsonName)}"] && [dict[@"${objectiveCStringEscape(jsonName)}"] rangeOfString:@"^[0-9]{4}-[0-9]{2}-[0-9]{2}T" options:NSRegularExpressionSearch].location == NSNotFound) return nil;`,
`if (dict[@"${objectiveCStringEscape(jsonName)}"] && [dict[@"${objectiveCStringEscape(jsonName)}"] rangeOfString:@"^[0-9]{4}-[0-9]{2}-[0-9]{2}[Tt](?:[01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:\\\\.[0-9]+)?(?:[Zz]|[+-](?:[01][0-9]|2[0-3]):[0-5][0-9])?$" options:NSRegularExpressionSearch].location == NSNotFound) return nil;`,
);
this.emitLine(
`if (dict[@"${objectiveCStringEscape(jsonName)}"] && ![[[[NSISO8601DateFormatter new] stringFromDate:[[NSISO8601DateFormatter new] dateFromString:[[dict[@"${objectiveCStringEscape(jsonName)}"] substringToIndex:10] stringByAppendingString:@"T00:00:00Z"]]] substringToIndex:10] isEqualToString:[dict[@"${objectiveCStringEscape(jsonName)}"] substringToIndex:10]]) return nil;`,
);
}
if (
Expand Down
8 changes: 8 additions & 0 deletions packages/quicktype-core/src/language/Php/PhpRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,14 @@ export class PhpRenderer extends ConvenienceRenderer {
(transformedStringType) => {
if (transformedStringType.kind === "date-time") {
this.emitLine("$tmp = ", "new DateTime(", args, ");");
this.emitLine("$errors = DateTime::getLastErrors();");
this.emitBlock(
"if ($errors && ($errors['warning_count'] || $errors['error_count']))",
() =>
this.emitLine(
"throw new Exception('Invalid date-time');",
),
);
this.transformDateTime(className, "", ["$tmp"]);
this.emitLine("return $tmp;");
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,7 @@ export class JSONPythonRenderer extends PythonRenderer {
this.emitLine(
"assert isinstance(x, str) and ",
this.withModuleImport("re"),
'.match(r"^\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})?$", x)',
'.match(r"^\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:[Zz]|[+-]\\d{2}:\\d{2})?$", x)',
);
this.emitLine("return dateutil.parser.parse(x).timetz()");
},
Expand All @@ -542,7 +542,7 @@ export class JSONPythonRenderer extends PythonRenderer {
],
() => {
this._haveDateutil = true;
this.emitLine("return dateutil.parser.parse(x)");
this.emitLine("return dateutil.parser.isoparse(x)");
},
);
}
Expand Down
12 changes: 8 additions & 4 deletions packages/quicktype-core/src/language/Swift/SwiftRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -749,15 +749,19 @@ export class SwiftRenderer extends ConvenienceRenderer {
this.emitBlock(
"if #available(iOS 10.0, OSX 10.12, tvOS 10.0, watchOS 3.0, *)",
() => {
this.emitLine(
"decoder.dateDecodingStrategy = .iso8601",
);
this.emitMultiline(`decoder.dateDecodingStrategy = .custom { decoder in
let dateStr = try decoder.singleValueContainer().decode(String.self).uppercased()
let dateData = try JSONEncoder().encode(dateStr)
let dateDecoder = JSONDecoder()
dateDecoder.dateDecodingStrategy = .iso8601
return try dateDecoder.decode(Date.self, from: dateData)
}`);
},
);
} else {
this.emitMultiline(`decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in
let container = try decoder.singleValueContainer()
let dateStr = try container.decode(String.self)
let dateStr = try container.decode(String.self).uppercased()

let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,11 @@ export class TypeScriptEffectSchemaRenderer extends ConvenienceRenderer {
? 'S.transform(S.Literal("true", "false"), S.Boolean, { strict: true, decode: (value) => value === "true", encode: (value) => value ? "true" : "false" })'
: 'S.Literal("true", "false")';
if (_transformedStringType.kind === "date")
return "S.String.pipe(S.pattern(/^\\d{4}-\\d{2}-\\d{2}$/))";
return 'S.String.pipe(S.pattern(/^\\d{4}-(?:0[1-9]|1[0-2])-(?:[0-2]\\d|3[01])$/), S.filter(value => (new Date(value + "T00:00:00Z").toJSON() || "").slice(0, 10) === value))';
if (_transformedStringType.kind === "time")
return "S.String.pipe(S.pattern(/^\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$/))";
return "S.String.pipe(S.pattern(/^(?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:Z|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$/i))";
if (_transformedStringType.kind === "date-time")
return "S.String.pipe(S.pattern(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$/))";
return 'S.String.pipe(S.pattern(/^\\d{4}-(?:0[1-9]|1[0-2])-(?:[0-2]\\d|3[01])T(?:[01]\\d|2[0-3]):[0-5]\\d:(?:[0-5]\\d|60)(?:\\.\\d+)?(?:Z|[+-](?:[01]\\d|2[0-3]):[0-5]\\d)$/i), S.filter(value => (new Date(value.slice(0, 10) + "T00:00:00Z").toJSON() || "").slice(0, 10) === value.slice(0, 10)))';
if (_transformedStringType.kind === "integer-string")
return coerceStrings
? "S.NumberFromString.pipe(S.int())"
Expand Down
7 changes: 7 additions & 0 deletions test/inputs/schema/date-time.12.fail.date-time.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"date": "1985-04-12",
"time": "23:20:50.52Z",
"date-time": "2023-00-15T12:00:00Z",
"union-array": ["1985-04-12", "23:20:50.52Z"],
"complex-union-array": ["2018-08-13T21:31:01+00:10", "foo", 123]
}
7 changes: 7 additions & 0 deletions test/inputs/schema/date-time.8.fail.date-time.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"date": "1985-04-12",
"time": "23:20:50.52Z",
"date-time": "1",
"union-array": ["1985-04-12", "23:20:50.52Z"],
"complex-union-array": ["2018-08-13T21:31:01+00:10", "foo", 123]
}
4 changes: 4 additions & 0 deletions test/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ export const PythonLanguage: Language = {
"union",
"no-defaults",
"date-time",
"date",
"time",
"integer-string",
"bool-string",
"uuid",
Expand Down Expand Up @@ -1787,6 +1789,8 @@ export const TypeScriptEffectSchemaLanguage: Language = {
"minmaxlength",
"bool-string",
"date-time",
"date",
"time",
"integer-string",
"pattern",
"minmax",
Expand Down
37 changes: 37 additions & 0 deletions test/unit/java-date-time-emission.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { InputData, JSONSchemaInput, quicktype } from "quicktype-core";
import { describe, expect, test } from "vitest";

async function renderJava(
propertySchema: object,
name = "TopLevel",
): Promise<string> {
const schemaInput = new JSONSchemaInput(undefined);
await schemaInput.addSource({
name,
schema: JSON.stringify({
type: "object",
properties: { value: propertySchema },
}),
});
const inputData = new InputData();
inputData.addInput(schemaInput);

return (await quicktype({ inputData, lang: "java" })).lines.join("\n");
}

describe("Java strict calendar validation", () => {
test("are emitted only for date-time types", async () => {
const plain = await renderJava({ type: "string" });
const dateTime = await renderJava({
type: "string",
format: "date-time",
});

expect(plain).not.toContain("ResolverStyle");
expect(await renderJava({ type: "string" }, "ResolverStyle")).toContain(
"class ResolverStyle",
);
expect(dateTime).toContain("ResolverStyle.STRICT");
expect(dateTime).toContain("DATE_TIME_FORMATTER");
});
});
42 changes: 42 additions & 0 deletions test/unit/javascript-date-api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { InputData, JSONSchemaInput, quicktype } from "quicktype-core";
import { expect, test } from "vitest";

interface GeneratedConverters {
topLevelToJson: (value: { when: Date }) => string;
}

async function converters(): Promise<GeneratedConverters> {
const schemaInput = new JSONSchemaInput(undefined);
await schemaInput.addSource({
name: "TopLevel",
schema: JSON.stringify({
type: "object",
properties: {
when: { type: "string", format: "date-time" },
},
required: ["when"],
}),
});
const inputData = new InputData();
inputData.addInput(schemaInput);
const result = await quicktype({ inputData, lang: "javascript" });
const generatedModule: { exports: Partial<GeneratedConverters> } = {
exports: {},
};
new Function("exports", "module", result.lines.join("\n"))(
generatedModule.exports,
generatedModule,
);
return generatedModule.exports as GeneratedConverters;
}

test("JavaScript converter validates Date instances", async () => {
const { topLevelToJson } = await converters();

expect(
JSON.parse(topLevelToJson({ when: new Date("2024-02-29T00:00:00Z") })),
).toEqual({ when: "2024-02-29T00:00:00.000Z" });
expect(() => topLevelToJson({ when: new Date(Number.NaN) })).toThrow(
"Expected Date",
);
});
Loading