diff --git a/bin/configs/typescript-fetch-date-library-date.yaml b/bin/configs/typescript-fetch-date-library-date.yaml new file mode 100644 index 000000000000..9d8211cca853 --- /dev/null +++ b/bin/configs/typescript-fetch-date-library-date.yaml @@ -0,0 +1,6 @@ +generatorName: typescript-fetch +outputDir: samples/client/petstore/typescript-fetch/builds/date-library-date +inputSpec: modules/openapi-generator/src/test/resources/3_0/typescript-fetch/date-handling.yaml +templateDir: modules/openapi-generator/src/main/resources/typescript-fetch +additionalProperties: + dateLibrary: date diff --git a/bin/configs/typescript-fetch-date-library-string.yaml b/bin/configs/typescript-fetch-date-library-string.yaml new file mode 100644 index 000000000000..09299efa3cb9 --- /dev/null +++ b/bin/configs/typescript-fetch-date-library-string.yaml @@ -0,0 +1,6 @@ +generatorName: typescript-fetch +outputDir: samples/client/petstore/typescript-fetch/builds/date-library-string +inputSpec: modules/openapi-generator/src/test/resources/3_0/typescript-fetch/date-handling.yaml +templateDir: modules/openapi-generator/src/main/resources/typescript-fetch +additionalProperties: + dateLibrary: string diff --git a/docs/generators/typescript-fetch.md b/docs/generators/typescript-fetch.md index 37762fabc0d9..a51cfe50c40d 100644 --- a/docs/generators/typescript-fetch.md +++ b/docs/generators/typescript-fetch.md @@ -19,6 +19,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl | Option | Description | Values | Default | | ------ | ----------- | ------ | ------- | |allowUnicodeIdentifiers|boolean, toggles whether unicode identifiers are allowed in names or not, default is false| |false| +|dateLibrary|Option. Date library to use.|
**date**
Native Date. `format: date` and `format: date-time` are both mapped to Date and (de)serialized by the runtime.
**string**
Plain string. Values are passed through untouched, leaving date handling to the consumer.
|date| |disallowAdditionalPropertiesIfNotPresent|If false, the 'additionalProperties' implementation (set to true by default) is compliant with the OAS and JSON schema specifications. If true (default), keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.|
**false**
The 'additionalProperties' implementation is compliant with the OAS and JSON schema specifications.
**true**
Keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.
|true| |ensureUniqueParams|Whether to ensure parameter names are unique in an operation (rename parameters that are not).| |true| |enumNameSuffix|Suffix that will be appended to all enum names.| |Enum| diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java index 5d9c7091ad49..21ce334aabd6 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java @@ -36,6 +36,8 @@ import org.openapitools.codegen.model.OperationsMap; import org.openapitools.codegen.templating.mustache.IndentedLambda; import org.openapitools.codegen.utils.ModelUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.File; import java.util.*; @@ -52,11 +54,17 @@ *

Mustache templates are located in {@code src/main/resources/typescript-fetch/}. */ public class TypeScriptFetchClientCodegen extends AbstractTypeScriptClientCodegen { + private final Logger LOGGER = LoggerFactory.getLogger(TypeScriptFetchClientCodegen.class); + public static final String NPM_REPOSITORY = "npmRepository"; public static final String WITH_INTERFACES = "withInterfaces"; public static final String USE_SINGLE_REQUEST_PARAMETER = "useSingleRequestParameter"; public static final String PREFIX_PARAMETER_INTERFACES = "prefixParameterInterfaces"; public static final String WITHOUT_RUNTIME_CHECKS = "withoutRuntimeChecks"; + public static final String DATE_LIBRARY = "dateLibrary"; + public static final String DATE_LIBRARY_DESC = "Option. Date library to use."; + public static final String DATE_LIBRARY_DATE = "date"; + public static final String DATE_LIBRARY_STRING = "string"; public static final String STRING_ENUMS = "stringEnums"; public static final String STRING_ENUMS_DESC = "Generate string enums instead of objects for enum values."; public static final String IMPORT_FILE_EXTENSION_SWITCH = "importFileExtension"; @@ -79,6 +87,7 @@ public class TypeScriptFetchClientCodegen extends AbstractTypeScriptClientCodege protected boolean addedApiIndex = false; protected boolean addedModelIndex = false; protected boolean withoutRuntimeChecks = false; + protected String dateLibrary = DATE_LIBRARY_DATE; protected boolean stringEnums = false; protected String fileNaming = PASCAL_CASE; protected String apiDocPath = "docs"; @@ -96,6 +105,7 @@ public class TypeScriptFetchClientCodegen extends AbstractTypeScriptClientCodege private static final String X_OPERATION_RETURN_PASSTHROUGH = "x-operationReturnPassthrough"; private static final String X_KEEP_AS_JS_OBJECT = "x-keepAsJSObject"; private static final String X_TYPESCRIPT_FETCH_API_EXAMPLE = "x-typescriptFetchApiExample"; + private static final String X_HAS_DATE_VARS = "x-hasDateVars"; private static final String BLOB_API_EXAMPLE = "new Blob(['example file content'], { type: 'application/octet-stream' })"; protected boolean sagasAndRecords = false; @@ -134,6 +144,13 @@ public TypeScriptFetchClientCodegen() { this.cliOptions.add(new CliOption(CodegenConstants.USE_SINGLE_REQUEST_PARAMETER, CodegenConstants.USE_SINGLE_REQUEST_PARAMETER_DESC, SchemaTypeUtil.BOOLEAN_TYPE).defaultValue(Boolean.TRUE.toString())); this.cliOptions.add(new CliOption(PREFIX_PARAMETER_INTERFACES, "Setting this property to true will generate parameter interface declarations prefixed with API class name to avoid name conflicts.", SchemaTypeUtil.BOOLEAN_TYPE).defaultValue(Boolean.FALSE.toString())); this.cliOptions.add(new CliOption(WITHOUT_RUNTIME_CHECKS, "Setting this property to true will remove any runtime checks on the request and response payloads. Payloads will be casted to their expected types.", SchemaTypeUtil.BOOLEAN_TYPE).defaultValue(Boolean.FALSE.toString())); + + CliOption dateLibraryOption = new CliOption(DATE_LIBRARY, DATE_LIBRARY_DESC).defaultValue(this.getDateLibrary()); + Map dateOptions = new HashMap<>(); + dateOptions.put(DATE_LIBRARY_DATE, "Native Date. `format: date` and `format: date-time` are both mapped to Date and (de)serialized by the runtime."); + dateOptions.put(DATE_LIBRARY_STRING, "Plain string. Values are passed through untouched, leaving date handling to the consumer."); + dateLibraryOption.setEnum(dateOptions); + this.cliOptions.add(dateLibraryOption); this.cliOptions.add(new CliOption(SAGAS_AND_RECORDS, "Setting this property to true will generate additional files for use with redux-saga and immutablejs.", SchemaTypeUtil.BOOLEAN_TYPE).defaultValue(Boolean.FALSE.toString())); this.cliOptions.add(new CliOption(STRING_ENUMS, STRING_ENUMS_DESC, SchemaTypeUtil.BOOLEAN_TYPE).defaultValue(Boolean.FALSE.toString())); this.cliOptions.add(new CliOption(IMPORT_FILE_EXTENSION_SWITCH, IMPORT_FILE_EXTENSION_SWITCH_DESC).defaultValue("")); @@ -192,6 +209,14 @@ public void setWithoutRuntimeChecks(Boolean withoutRuntimeChecks) { this.withoutRuntimeChecks = withoutRuntimeChecks; } + public String getDateLibrary() { + return this.dateLibrary; + } + + public void setDateLibrary(String dateLibrary) { + this.dateLibrary = dateLibrary; + } + public Boolean getStringEnums() { return this.stringEnums; } @@ -306,11 +331,34 @@ public void processOpts() { this.setFileNaming(additionalProperties.get(FILE_NAMING).toString()); } + if (additionalProperties.containsKey(DATE_LIBRARY)) { + this.setDateLibrary(additionalProperties.get(DATE_LIBRARY).toString()); + } + if (!withoutRuntimeChecks) { this.modelTemplateFiles.put("models.mustache", ".ts"); + } + + // `date` needs the model (de)serialization to convert with, which + // withoutRuntimeChecks removes: the raw string would just be cast to Date. + if (withoutRuntimeChecks && DATE_LIBRARY_DATE.equals(this.dateLibrary)) { + if (additionalProperties.containsKey(DATE_LIBRARY)) { + LOGGER.warn("{}={} is not compatible with {}=true; falling back to {}={}.", + DATE_LIBRARY, DATE_LIBRARY_DATE, WITHOUT_RUNTIME_CHECKS, DATE_LIBRARY, DATE_LIBRARY_STRING); + } + this.dateLibrary = DATE_LIBRARY_STRING; + } + + if (DATE_LIBRARY_DATE.equals(this.dateLibrary)) { typeMapping.put("date", "Date"); typeMapping.put("DateTime", "Date"); + } else { + typeMapping.put("date", "string"); + typeMapping.put("DateTime", "string"); } + additionalProperties.put(DATE_LIBRARY, this.dateLibrary); + // Mustache cannot compare strings, so expose the selected library as a flag. + additionalProperties.put("isDateLibraryDate", DATE_LIBRARY_DATE.equals(this.dateLibrary)); if (additionalProperties.containsKey(SAGAS_AND_RECORDS)) { this.setSagasAndRecords(convertPropertyToBoolean(SAGAS_AND_RECORDS)); @@ -407,6 +455,12 @@ public ModelsMap postProcessModels(ModelsMap objs) { ExtendedCodegenModel cm = (ExtendedCodegenModel) mo.getModel(); cm.imports = new TreeSet<>(cm.imports); this.processCodeGenModel(cm); + // Mirrors the branches in modelGeneric.mustache that call the date helpers, so a + // model without dates does not import them. + cm.vendorExtensions.put(X_HAS_DATE_VARS, cm.vars.stream() + .filter(ExtendedCodegenProperty.class::isInstance) + .map(ExtendedCodegenProperty.class::cast) + .anyMatch(v -> v.isPrimitiveType && !v.isArray && (v.isDateType() || v.isDateTimeType()))); } // Add supporting file only if we plan to generate files in /models diff --git a/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache b/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache index 97fb15df5253..d88cecad8505 100644 --- a/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache @@ -272,9 +272,13 @@ export class {{classname}} extends runtime.BaseAPI { {{^isArray}} if (requestParameters['{{paramName}}'] != null) { {{#isDateTimeType}} - formParams.append('{{baseName}}', (requestParameters['{{paramName}}'] as any).toISOString()); + formParams.append('{{baseName}}', runtime.serializeDateTime(requestParameters['{{paramName}}'] as any)); {{/isDateTimeType}} {{^isDateTimeType}} + {{#isDateType}} + formParams.append('{{baseName}}', runtime.serializeDate(requestParameters['{{paramName}}'] as any)); + {{/isDateType}} + {{^isDateType}} {{#isPrimitiveType}} formParams.append('{{baseName}}', requestParameters['{{paramName}}'] as any); {{/isPrimitiveType}} @@ -295,6 +299,7 @@ export class {{classname}} extends runtime.BaseAPI { {{/withoutRuntimeChecks}} {{/isEnumRef}} {{/isPrimitiveType}} + {{/isDateType}} {{/isDateTimeType}} } @@ -306,7 +311,7 @@ export class {{classname}} extends runtime.BaseAPI { {{#pathParams}} {{#isDateTimeType}} if (requestParameters['{{paramName}}'] instanceof Date) { - urlPath = urlPath.replace({{=<< >>=}}'{<>}'<<={{ }}=>>, encodeURIComponent(requestParameters['{{paramName}}'].toISOString())); + urlPath = urlPath.replace({{=<< >>=}}'{<>}'<<={{ }}=>>, encodeURIComponent(runtime.serializeDateTime(requestParameters['{{paramName}}']))); } else { urlPath = urlPath.replace({{=<< >>=}}'{<>}'<<={{ }}=>>, encodeURIComponent(String(requestParameters['{{paramName}}']))); } @@ -314,7 +319,7 @@ export class {{classname}} extends runtime.BaseAPI { {{^isDateTimeType}} {{#isDateType}} if (requestParameters['{{paramName}}'] instanceof Date) { - urlPath = urlPath.replace({{=<< >>=}}'{<>}'<<={{ }}=>>, encodeURIComponent(requestParameters['{{paramName}}'].toISOString().substring(0,10))); + urlPath = urlPath.replace({{=<< >>=}}'{<>}'<<={{ }}=>>, encodeURIComponent(runtime.serializeDate(requestParameters['{{paramName}}']))); } else { urlPath = urlPath.replace({{=<< >>=}}'{<>}'<<={{ }}=>>, encodeURIComponent(String(requestParameters['{{paramName}}']))); } diff --git a/modules/openapi-generator/src/main/resources/typescript-fetch/apisAssignQueryParam.mustache b/modules/openapi-generator/src/main/resources/typescript-fetch/apisAssignQueryParam.mustache index 8da37374eea1..761528af2546 100644 --- a/modules/openapi-generator/src/main/resources/typescript-fetch/apisAssignQueryParam.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-fetch/apisAssignQueryParam.mustache @@ -1,10 +1,10 @@ {{! Assign query parameters based on their type }} {{#isDateTimeType}} - queryParameters['{{baseName}}'] = (requestParameters['{{paramName}}'] as any).toISOString(); + queryParameters['{{baseName}}'] = runtime.serializeDateTime(requestParameters['{{paramName}}'] as any); {{/isDateTimeType}} {{^isDateTimeType}} {{#isDateType}} - queryParameters['{{baseName}}'] = (requestParameters['{{paramName}}'] as any).toISOString().substring(0,10); + queryParameters['{{baseName}}'] = runtime.serializeDate(requestParameters['{{paramName}}'] as any); {{/isDateType}} {{^isDateType}} queryParameters['{{baseName}}'] = requestParameters['{{paramName}}']; diff --git a/modules/openapi-generator/src/main/resources/typescript-fetch/modelGeneric.mustache b/modules/openapi-generator/src/main/resources/typescript-fetch/modelGeneric.mustache index 5f4c379724b1..2e511274a4fa 100644 --- a/modules/openapi-generator/src/main/resources/typescript-fetch/modelGeneric.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-fetch/modelGeneric.mustache @@ -1,4 +1,4 @@ -import { mapValues } from '../runtime{{importFileExtension}}'; +import { mapValues{{#isDateLibraryDate}}{{#vendorExtensions.x-hasDateVars}}, parseDate, parseDateTime, serializeDate, serializeDateTime{{/vendorExtensions.x-hasDateVars}}{{/isDateLibraryDate}} } from '../runtime{{importFileExtension}}'; {{#hasImports}} {{#tsImports}} import type { {{{classname}}} } from './{{filename}}{{importFileExtension}}'; @@ -98,10 +98,10 @@ export function {{classname}}FromJSONTyped(json: any, ignoreDiscriminator: boole {{/isArray}} {{^isArray}} {{#isDateType}} - '{{name}}': {{^required}}{{#isNullable}}json['{{baseName}}'] === undefined ? undefined : json['{{baseName}}'] === null ? null : {{/isNullable}}{{^isNullable}}json['{{baseName}}'] == null ? undefined : {{/isNullable}}{{/required}}({{#required}}{{#isNullable}}json['{{baseName}}'] == null ? null : {{/isNullable}}{{/required}}new Date(json['{{baseName}}'])), + '{{name}}': {{^required}}{{#isNullable}}json['{{baseName}}'] === undefined ? undefined : json['{{baseName}}'] === null ? null : {{/isNullable}}{{^isNullable}}json['{{baseName}}'] == null ? undefined : {{/isNullable}}{{/required}}({{#required}}{{#isNullable}}json['{{baseName}}'] == null ? null : {{/isNullable}}{{/required}}parseDate(json['{{baseName}}'])), {{/isDateType}} {{#isDateTimeType}} - '{{name}}': {{^required}}{{#isNullable}}json['{{baseName}}'] === undefined ? undefined : json['{{baseName}}'] === null ? null : {{/isNullable}}{{^isNullable}}json['{{baseName}}'] == null ? undefined : {{/isNullable}}{{/required}}({{#required}}{{#isNullable}}json['{{baseName}}'] == null ? null : {{/isNullable}}{{/required}}new Date(json['{{baseName}}'])), + '{{name}}': {{^required}}{{#isNullable}}json['{{baseName}}'] === undefined ? undefined : json['{{baseName}}'] === null ? null : {{/isNullable}}{{^isNullable}}json['{{baseName}}'] == null ? undefined : {{/isNullable}}{{/required}}({{#required}}{{#isNullable}}json['{{baseName}}'] == null ? null : {{/isNullable}}{{/required}}parseDateTime(json['{{baseName}}'])), {{/isDateTimeType}} {{^isDateType}} {{^isDateTimeType}} @@ -173,10 +173,10 @@ export function {{classname}}ToJSONTyped(value?: {{#hasReadOnly}}Omit<{{classnam {{^isReadOnly}} {{#isPrimitiveType}} {{#isDateType}} - '{{baseName}}': {{^required}}value['{{name}}'] == null ? value['{{name}}'] : {{/required}}{{#isNullable}}{{#required}}value['{{name}}'] == null ? value['{{name}}'] : {{/required}}{{/isNullable}}value['{{name}}'].toISOString().substring(0,10), + '{{baseName}}': {{^required}}value['{{name}}'] == null ? value['{{name}}'] : {{/required}}{{#isNullable}}{{#required}}value['{{name}}'] == null ? value['{{name}}'] : {{/required}}{{/isNullable}}serializeDate(value['{{name}}']), {{/isDateType}} {{#isDateTimeType}} - '{{baseName}}': {{^required}}value['{{name}}'] == null ? value['{{name}}'] : {{/required}}{{#isNullable}}{{#required}}value['{{name}}'] == null ? value['{{name}}'] : {{/required}}{{/isNullable}}value['{{name}}'].toISOString(), + '{{baseName}}': {{^required}}value['{{name}}'] == null ? value['{{name}}'] : {{/required}}{{#isNullable}}{{#required}}value['{{name}}'] == null ? value['{{name}}'] : {{/required}}{{/isNullable}}serializeDateTime(value['{{name}}']), {{/isDateTimeType}} {{#isArray}} '{{baseName}}': {{#uniqueItems}}{{^required}}value['{{name}}'] == null ? undefined : {{/required}}{{#required}}{{#isNullable}}value['{{name}}'] == null ? null : {{/isNullable}}{{/required}}Array.from(value['{{name}}'] as Set){{/uniqueItems}}{{^uniqueItems}}value['{{name}}']{{/uniqueItems}}, diff --git a/modules/openapi-generator/src/main/resources/typescript-fetch/modelOneOf.mustache b/modules/openapi-generator/src/main/resources/typescript-fetch/modelOneOf.mustache index 7017451090f8..4d2f4ce40baa 100644 --- a/modules/openapi-generator/src/main/resources/typescript-fetch/modelOneOf.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-fetch/modelOneOf.mustache @@ -1,3 +1,6 @@ +{{#isDateLibraryDate}} +import { parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime{{importFileExtension}}'; +{{/isDateLibraryDate}} {{#hasImports}} {{#oneOfArrays}} import type { {{{.}}} } from './{{.}}{{importFileExtension}}'; @@ -70,14 +73,14 @@ export function {{classname}}FromJSONTyped(json: any, ignoreDiscriminator: boole {{#isDateType}} if (Array.isArray(json)) { if (json.every(item => !(isNaN(new Date(item).getTime())))) { - return json.map(value => new Date(value)); + return json.map(value => parseDate(value)); } } {{/isDateType}} {{#isDateTimeType}} if (Array.isArray(json)) { if (json.every(item => !(isNaN(new Date(item).getTime())))) { - return json.map(value => new Date(value)); + return json.map(value => parseDateTime(value)); } } {{/isDateTimeType}} @@ -116,13 +119,13 @@ export function {{classname}}FromJSONTyped(json: any, ignoreDiscriminator: boole {{^isArray}} {{#isDateType}} if (!(isNaN(new Date(json).getTime()))) { - return {{^required}}json == null ? undefined : {{/required}}({{#required}}{{#isNullable}}json == null ? null : {{/isNullable}}{{/required}}new Date(json)); + return {{^required}}json == null ? undefined : {{/required}}({{#required}}{{#isNullable}}json == null ? null : {{/isNullable}}{{/required}}parseDate(json)); } {{/isDateType}} {{^isDateType}} {{#isDateTimeType}} if (!(isNaN(new Date(json).getTime()))) { - return {{^required}}json == null ? undefined : {{/required}}({{#required}}{{#isNullable}}json == null ? null : {{/isNullable}}{{/required}}new Date(json)); + return {{^required}}json == null ? undefined : {{/required}}({{#required}}{{#isNullable}}json == null ? null : {{/isNullable}}{{/required}}parseDateTime(json)); } {{/isDateTimeType}} {{/isDateType}} @@ -195,14 +198,14 @@ export function {{classname}}ToJSONTyped(value?: {{classname}} | null, ignoreDis {{#isDateType}} if (Array.isArray(value)) { if (value.every(item => item instanceof Date)) { - return value.map(value => value.toISOString().substring(0,10)); + return value.map(value => serializeDate(value)); } } {{/isDateType}} {{#isDateTimeType}} if (Array.isArray(value)) { if (value.every(item => item instanceof Date)) { - return value.map(item => item.toISOString()); + return value.map(item => serializeDateTime(item)); } } {{/isDateTimeType}} @@ -241,12 +244,12 @@ export function {{classname}}ToJSONTyped(value?: {{classname}} | null, ignoreDis {{^isArray}} {{#isDateType}} if (value instanceof Date) { - return ((value{{#isNullable}} as any{{/isNullable}}){{^required}}{{#isNullable}}?{{/isNullable}}{{/required}}.toISOString().substring(0,10)); + return (serializeDate(value{{#isNullable}} as any{{/isNullable}})); } {{/isDateType}} {{#isDateTimeType}} if (value instanceof Date) { - return {{^required}}{{#isNullable}}value === null ? null : {{/isNullable}}{{^isNullable}}value == null ? undefined : {{/isNullable}}{{/required}}((value{{#isNullable}} as any{{/isNullable}}){{^required}}{{#isNullable}}?{{/isNullable}}{{/required}}.toISOString()); + return {{^required}}{{#isNullable}}value === null ? null : {{/isNullable}}{{^isNullable}}value == null ? undefined : {{/isNullable}}{{/required}}(serializeDateTime(value{{#isNullable}} as any{{/isNullable}})); } {{/isDateTimeType}} {{#isNumeric}} diff --git a/modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache b/modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache index 235458a2df2f..9dfa41c5774b 100644 --- a/modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache +++ b/modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache @@ -336,7 +336,7 @@ function querystringSingleKey(key: string, value: string | number | null | undef return querystringSingleKey(key, valueAsArray, keyPrefix); } if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; } if (value instanceof Object) { return querystring(value as HTTPQuery, fullKey); @@ -349,6 +349,60 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + +{{#isDateLibraryDate}} +export function serializeDate(value: Date): string { + if (isNaN(value.getTime())) { + throw new RangeError('Invalid time value'); + } + // Not padStart: the generated client may target ES6, where it does not exist. + const year = ('000' + value.getFullYear()).slice(-4); + const month = ('0' + (value.getMonth() + 1)).slice(-2); + const day = ('0' + value.getDate()).slice(-2); + return `${year}-${month}-${day}`; +} + + +export function parseDate(value: any): Date { + if (value instanceof Date) { + return value; + } + // `new Date("2026-08-05")` would parse as UTC midnight: a different day west of UTC. + // Local midnight is the stated day everywhere. setFullYear avoids the 1900 offset the + // multi-argument constructor applies to years 0-99. + const fullDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value)); + if (fullDate) { + const year = Number(fullDate[1]); + const month = Number(fullDate[2]) - 1; + const day = Number(fullDate[3]); + const date = new Date(0); + date.setFullYear(year, month, day); + date.setHours(0, 0, 0, 0); + // Out-of-range components (or a day the local zone skipped) silently roll over, + // which would hand back a date the server never sent. + if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { + return new Date(NaN); + } + return date; + } + return new Date(value); +} + +export function parseDateTime(value: any): Date { + return new Date(value); +} +{{/isDateLibraryDate}} + {{^withoutRuntimeChecks}} export function mapValues(data: any, fn: (item: any) => any) { const result: { [key: string]: any } = {}; diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/TypeScriptFetchClientOptionsProvider.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/TypeScriptFetchClientOptionsProvider.java index a6bccce568cb..9b35b5074d35 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/TypeScriptFetchClientOptionsProvider.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/options/TypeScriptFetchClientOptionsProvider.java @@ -31,6 +31,7 @@ public class TypeScriptFetchClientOptionsProvider implements TypeScriptSharedCli public static final String SAGAS_AND_RECORDS = "false"; public static final String STRING_ENUMS = "false"; public static final String FILE_NAMING_VALUE = PASCAL_CASE; + public static final String DATE_LIBRARY_VALUE = TypeScriptFetchClientCodegen.DATE_LIBRARY_STRING; @Override @@ -50,6 +51,7 @@ public Map createOptions() { .put(TypeScriptFetchClientCodegen.SAGAS_AND_RECORDS, SAGAS_AND_RECORDS) .put(TypeScriptFetchClientCodegen.IMPORT_FILE_EXTENSION_SWITCH, IMPORT_FILE_EXTENSION_VALUE) .put(TypeScriptFetchClientCodegen.FILE_NAMING, FILE_NAMING_VALUE) + .put(TypeScriptFetchClientCodegen.DATE_LIBRARY, DATE_LIBRARY_VALUE) .put(TypeScriptFetchClientCodegen.STRING_ENUMS, STRING_ENUMS) .put(TypeScriptFetchClientCodegen.USE_SQUARE_BRACKETS_IN_ARRAY_NAMES, Boolean.FALSE.toString()) .put(TypeScriptFetchClientCodegen.VALIDATION_ATTRIBUTES, Boolean.FALSE.toString()) diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientCodegenTest.java index 0ba3cd8a5305..7ca392559a83 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientCodegenTest.java @@ -765,6 +765,76 @@ private static File generate( ); } + @Test(description = "Verify dateLibrary=date (the default) maps date and date-time to Date and converts them through the runtime helpers") + public void testDateLibraryDateIsTheDefault() throws IOException { + File output = generate(new HashMap<>(), DATE_HANDLING_SPEC); + + Path event = Paths.get(output + "/models/Event.ts"); + TestUtils.assertFileContains(event, "startsOn: Date;"); + TestUtils.assertFileContains(event, "createdAt?: Date;"); + TestUtils.assertFileContains(event, "'startsOn': (parseDate(json['startsOn']))"); + TestUtils.assertFileContains(event, "'createdAt': json['createdAt'] == null ? undefined : (parseDateTime(json['createdAt']))"); + TestUtils.assertFileContains(event, "'startsOn': serializeDate(value['startsOn'])"); + + Path runtime = Paths.get(output + "/runtime.ts"); + TestUtils.assertFileContains(runtime, "export function parseDate("); + TestUtils.assertFileContains(runtime, "export function parseDateTime("); + + // A model without a date must not import the helpers it cannot use. + Path venue = Paths.get(output + "/models/Venue.ts"); + TestUtils.assertFileContains(venue, "import { mapValues } from '../runtime';"); + } + + @Test(description = "Verify dateLibrary=string leaves date values untouched as strings") + public void testDateLibraryString() throws IOException { + Map properties = new HashMap<>(); + properties.put(TypeScriptFetchClientCodegen.DATE_LIBRARY, TypeScriptFetchClientCodegen.DATE_LIBRARY_STRING); + + File output = generate(properties, DATE_HANDLING_SPEC); + + Path event = Paths.get(output + "/models/Event.ts"); + TestUtils.assertFileContains(event, "startsOn: string;"); + TestUtils.assertFileContains(event, "createdAt?: string;"); + TestUtils.assertFileContains(event, "'startsOn': json['startsOn'],"); + TestUtils.assertFileNotContains(event, "parseDate"); + TestUtils.assertFileNotContains(event, "serializeDate"); + + // Only the helper querystring needs is emitted. + Path runtime = Paths.get(output + "/runtime.ts"); + TestUtils.assertFileNotContains(runtime, "export function parseDate("); + TestUtils.assertFileNotContains(runtime, "export function parseDateTime("); + TestUtils.assertFileNotContains(runtime, "export function serializeDate("); + TestUtils.assertFileContains(runtime, "export function serializeDateTime("); + } + + @Test(description = "Verify withoutRuntimeChecks forces dateLibrary=string, since there is no model code left to convert with") + public void testDateLibraryDateFallsBackToStringWithoutRuntimeChecks() throws IOException { + Map properties = new HashMap<>(); + properties.put(TypeScriptFetchClientCodegen.WITHOUT_RUNTIME_CHECKS, true); + properties.put(TypeScriptFetchClientCodegen.DATE_LIBRARY, TypeScriptFetchClientCodegen.DATE_LIBRARY_DATE); + + File output = generate(properties, DATE_HANDLING_SPEC); + + Path modelsIndex = Paths.get(output + "/models/index.ts"); + TestUtils.assertFileContains(modelsIndex, "startsOn: string;"); + TestUtils.assertFileNotContains(modelsIndex, "startsOn: Date;"); + } + + @Test(description = "Verify format: date is serialized as a calendar date in every parameter location, not as a date-time") + public void testDateFormatIsSerializedAsACalendarDate() throws IOException { + File output = generate(new HashMap<>(), DATE_HANDLING_SPEC); + + Path api = Paths.get(output + "/apis/DefaultApi.ts"); + TestUtils.assertFileContains(api, "urlPath.replace('{onDate}', encodeURIComponent(runtime.serializeDate(requestParameters['onDate'])))"); + TestUtils.assertFileContains(api, "queryParameters['from'] = runtime.serializeDate(requestParameters['from'] as any)"); + TestUtils.assertFileContains(api, "formParams.append('startsOn', runtime.serializeDate(requestParameters['startsOn'] as any))"); + // date-time keeps the full timestamp. + TestUtils.assertFileContains(api, "queryParameters['updatedSince'] = runtime.serializeDateTime(requestParameters['updatedSince'] as any)"); + TestUtils.assertFileContains(api, "formParams.append('createdAt', runtime.serializeDateTime(requestParameters['createdAt'] as any))"); + } + + private static final String DATE_HANDLING_SPEC = "src/test/resources/3_0/typescript-fetch/date-handling.yaml"; + private static File generate( Map properties, String inputSpec diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientOptionsTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientOptionsTest.java index c44b33dbd4c1..711e09fcf993 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientOptionsTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/fetch/TypeScriptFetchClientOptionsTest.java @@ -55,6 +55,7 @@ protected void verifyOptions() { verify(clientCodegen).setEnumUnknownDefaultCase(Boolean.parseBoolean(TypeScriptFetchClientOptionsProvider.ENUM_UNKNOWN_DEFAULT_CASE_VALUE)); verify(clientCodegen).setStringEnums(Boolean.parseBoolean(TypeScriptFetchClientOptionsProvider.STRING_ENUMS)); verify(clientCodegen).setFileNaming(TypeScriptFetchClientOptionsProvider.FILE_NAMING_VALUE); + verify(clientCodegen).setDateLibrary(TypeScriptFetchClientOptionsProvider.DATE_LIBRARY_VALUE); } @Test(description = "Verify if an exception is thrown when invalid values are used with fileNaming option") diff --git a/modules/openapi-generator/src/test/resources/3_0/typescript-fetch/date-handling.yaml b/modules/openapi-generator/src/test/resources/3_0/typescript-fetch/date-handling.yaml new file mode 100644 index 000000000000..3ede69f87f75 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/typescript-fetch/date-handling.yaml @@ -0,0 +1,87 @@ +openapi: 3.0.3 +info: + title: Date handling + description: > + Covers every location a `format: date` or `format: date-time` value can appear in, + so the generated (de)serialization can be checked in one place. + version: 1.0.0 +paths: + /events/{onDate}: + get: + operationId: listEvents + parameters: + - name: onDate + in: path + required: true + schema: + type: string + format: date + - name: from + in: query + required: false + schema: + type: string + format: date + - name: updatedSince + in: query + required: false + schema: + type: string + format: date-time + responses: + '200': + description: matching events + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Event' + /events: + post: + operationId: createEvent + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + required: + - startsOn + properties: + startsOn: + type: string + format: date + createdAt: + type: string + format: date-time + responses: + '200': + description: the created event + content: + application/json: + schema: + $ref: '#/components/schemas/Event' +components: + schemas: + Event: + type: object + required: + - startsOn + properties: + startsOn: + type: string + format: date + endsOn: + type: string + format: date + nullable: true + createdAt: + type: string + format: date-time + Venue: + description: Has no date of any kind, so it must not import the date helpers. + type: object + properties: + name: + type: string diff --git a/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/runtime.ts b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/runtime.ts index cc66cbfbb3af..52194da6fd93 100644 --- a/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/runtime.ts +++ b/samples/client/others/typescript-fetch/additional-properties-in-multipart-issue/runtime.ts @@ -346,7 +346,7 @@ function querystringSingleKey(key: string, value: string | number | null | undef return querystringSingleKey(key, valueAsArray, keyPrefix); } if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; } if (value instanceof Object) { return querystring(value as HTTPQuery, fullKey); @@ -359,6 +359,58 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + +export function serializeDate(value: Date): string { + if (isNaN(value.getTime())) { + throw new RangeError('Invalid time value'); + } + // Not padStart: the generated client may target ES6, where it does not exist. + const year = ('000' + value.getFullYear()).slice(-4); + const month = ('0' + (value.getMonth() + 1)).slice(-2); + const day = ('0' + value.getDate()).slice(-2); + return `${year}-${month}-${day}`; +} + + +export function parseDate(value: any): Date { + if (value instanceof Date) { + return value; + } + // `new Date("2026-08-05")` would parse as UTC midnight: a different day west of UTC. + // Local midnight is the stated day everywhere. setFullYear avoids the 1900 offset the + // multi-argument constructor applies to years 0-99. + const fullDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value)); + if (fullDate) { + const year = Number(fullDate[1]); + const month = Number(fullDate[2]) - 1; + const day = Number(fullDate[3]); + const date = new Date(0); + date.setFullYear(year, month, day); + date.setHours(0, 0, 0, 0); + // Out-of-range components (or a day the local zone skipped) silently roll over, + // which would hand back a date the server never sent. + if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { + return new Date(NaN); + } + return date; + } + return new Date(value); +} + +export function parseDateTime(value: any): Date { + return new Date(value); +} + export function mapValues(data: any, fn: (item: any) => any) { const result: { [key: string]: any } = {}; for (const key of Object.keys(data)) { diff --git a/samples/client/others/typescript-fetch/infinite-recursion-issue/runtime.ts b/samples/client/others/typescript-fetch/infinite-recursion-issue/runtime.ts index cc66cbfbb3af..52194da6fd93 100644 --- a/samples/client/others/typescript-fetch/infinite-recursion-issue/runtime.ts +++ b/samples/client/others/typescript-fetch/infinite-recursion-issue/runtime.ts @@ -346,7 +346,7 @@ function querystringSingleKey(key: string, value: string | number | null | undef return querystringSingleKey(key, valueAsArray, keyPrefix); } if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; } if (value instanceof Object) { return querystring(value as HTTPQuery, fullKey); @@ -359,6 +359,58 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + +export function serializeDate(value: Date): string { + if (isNaN(value.getTime())) { + throw new RangeError('Invalid time value'); + } + // Not padStart: the generated client may target ES6, where it does not exist. + const year = ('000' + value.getFullYear()).slice(-4); + const month = ('0' + (value.getMonth() + 1)).slice(-2); + const day = ('0' + value.getDate()).slice(-2); + return `${year}-${month}-${day}`; +} + + +export function parseDate(value: any): Date { + if (value instanceof Date) { + return value; + } + // `new Date("2026-08-05")` would parse as UTC midnight: a different day west of UTC. + // Local midnight is the stated day everywhere. setFullYear avoids the 1900 offset the + // multi-argument constructor applies to years 0-99. + const fullDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value)); + if (fullDate) { + const year = Number(fullDate[1]); + const month = Number(fullDate[2]) - 1; + const day = Number(fullDate[3]); + const date = new Date(0); + date.setFullYear(year, month, day); + date.setHours(0, 0, 0, 0); + // Out-of-range components (or a day the local zone skipped) silently roll over, + // which would hand back a date the server never sent. + if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { + return new Date(NaN); + } + return date; + } + return new Date(value); +} + +export function parseDateTime(value: any): Date { + return new Date(value); +} + export function mapValues(data: any, fn: (item: any) => any) { const result: { [key: string]: any } = {}; for (const key of Object.keys(data)) { diff --git a/samples/client/others/typescript-fetch/multipart-file-array/runtime.ts b/samples/client/others/typescript-fetch/multipart-file-array/runtime.ts index a49c6563b888..c7da03948db3 100644 --- a/samples/client/others/typescript-fetch/multipart-file-array/runtime.ts +++ b/samples/client/others/typescript-fetch/multipart-file-array/runtime.ts @@ -346,7 +346,7 @@ function querystringSingleKey(key: string, value: string | number | null | undef return querystringSingleKey(key, valueAsArray, keyPrefix); } if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; } if (value instanceof Object) { return querystring(value as HTTPQuery, fullKey); @@ -359,6 +359,58 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + +export function serializeDate(value: Date): string { + if (isNaN(value.getTime())) { + throw new RangeError('Invalid time value'); + } + // Not padStart: the generated client may target ES6, where it does not exist. + const year = ('000' + value.getFullYear()).slice(-4); + const month = ('0' + (value.getMonth() + 1)).slice(-2); + const day = ('0' + value.getDate()).slice(-2); + return `${year}-${month}-${day}`; +} + + +export function parseDate(value: any): Date { + if (value instanceof Date) { + return value; + } + // `new Date("2026-08-05")` would parse as UTC midnight: a different day west of UTC. + // Local midnight is the stated day everywhere. setFullYear avoids the 1900 offset the + // multi-argument constructor applies to years 0-99. + const fullDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value)); + if (fullDate) { + const year = Number(fullDate[1]); + const month = Number(fullDate[2]) - 1; + const day = Number(fullDate[3]); + const date = new Date(0); + date.setFullYear(year, month, day); + date.setHours(0, 0, 0, 0); + // Out-of-range components (or a day the local zone skipped) silently roll over, + // which would hand back a date the server never sent. + if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { + return new Date(NaN); + } + return date; + } + return new Date(value); +} + +export function parseDateTime(value: any): Date { + return new Date(value); +} + export function mapValues(data: any, fn: (item: any) => any) { const result: { [key: string]: any } = {}; for (const key of Object.keys(data)) { diff --git a/samples/client/others/typescript-fetch/self-import-issue/runtime.ts b/samples/client/others/typescript-fetch/self-import-issue/runtime.ts index 8aa237368f4f..54d1f049f19c 100644 --- a/samples/client/others/typescript-fetch/self-import-issue/runtime.ts +++ b/samples/client/others/typescript-fetch/self-import-issue/runtime.ts @@ -346,7 +346,7 @@ function querystringSingleKey(key: string, value: string | number | null | undef return querystringSingleKey(key, valueAsArray, keyPrefix); } if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; } if (value instanceof Object) { return querystring(value as HTTPQuery, fullKey); @@ -359,6 +359,58 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + +export function serializeDate(value: Date): string { + if (isNaN(value.getTime())) { + throw new RangeError('Invalid time value'); + } + // Not padStart: the generated client may target ES6, where it does not exist. + const year = ('000' + value.getFullYear()).slice(-4); + const month = ('0' + (value.getMonth() + 1)).slice(-2); + const day = ('0' + value.getDate()).slice(-2); + return `${year}-${month}-${day}`; +} + + +export function parseDate(value: any): Date { + if (value instanceof Date) { + return value; + } + // `new Date("2026-08-05")` would parse as UTC midnight: a different day west of UTC. + // Local midnight is the stated day everywhere. setFullYear avoids the 1900 offset the + // multi-argument constructor applies to years 0-99. + const fullDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value)); + if (fullDate) { + const year = Number(fullDate[1]); + const month = Number(fullDate[2]) - 1; + const day = Number(fullDate[3]); + const date = new Date(0); + date.setFullYear(year, month, day); + date.setHours(0, 0, 0, 0); + // Out-of-range components (or a day the local zone skipped) silently roll over, + // which would hand back a date the server never sent. + if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { + return new Date(NaN); + } + return date; + } + return new Date(value); +} + +export function parseDateTime(value: any): Date { + return new Date(value); +} + export function mapValues(data: any, fn: (item: any) => any) { const result: { [key: string]: any } = {}; for (const key of Object.keys(data)) { diff --git a/samples/client/petstore/typescript-fetch/builds/allOf-nullable/runtime.ts b/samples/client/petstore/typescript-fetch/builds/allOf-nullable/runtime.ts index a9a8ee84339c..3ceb3d69ab0e 100644 --- a/samples/client/petstore/typescript-fetch/builds/allOf-nullable/runtime.ts +++ b/samples/client/petstore/typescript-fetch/builds/allOf-nullable/runtime.ts @@ -346,7 +346,7 @@ function querystringSingleKey(key: string, value: string | number | null | undef return querystringSingleKey(key, valueAsArray, keyPrefix); } if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; } if (value instanceof Object) { return querystring(value as HTTPQuery, fullKey); @@ -359,6 +359,58 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + +export function serializeDate(value: Date): string { + if (isNaN(value.getTime())) { + throw new RangeError('Invalid time value'); + } + // Not padStart: the generated client may target ES6, where it does not exist. + const year = ('000' + value.getFullYear()).slice(-4); + const month = ('0' + (value.getMonth() + 1)).slice(-2); + const day = ('0' + value.getDate()).slice(-2); + return `${year}-${month}-${day}`; +} + + +export function parseDate(value: any): Date { + if (value instanceof Date) { + return value; + } + // `new Date("2026-08-05")` would parse as UTC midnight: a different day west of UTC. + // Local midnight is the stated day everywhere. setFullYear avoids the 1900 offset the + // multi-argument constructor applies to years 0-99. + const fullDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value)); + if (fullDate) { + const year = Number(fullDate[1]); + const month = Number(fullDate[2]) - 1; + const day = Number(fullDate[3]); + const date = new Date(0); + date.setFullYear(year, month, day); + date.setHours(0, 0, 0, 0); + // Out-of-range components (or a day the local zone skipped) silently roll over, + // which would hand back a date the server never sent. + if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { + return new Date(NaN); + } + return date; + } + return new Date(value); +} + +export function parseDateTime(value: any): Date { + return new Date(value); +} + export function mapValues(data: any, fn: (item: any) => any) { const result: { [key: string]: any } = {}; for (const key of Object.keys(data)) { diff --git a/samples/client/petstore/typescript-fetch/builds/allOf-readonly/runtime.ts b/samples/client/petstore/typescript-fetch/builds/allOf-readonly/runtime.ts index a9a8ee84339c..3ceb3d69ab0e 100644 --- a/samples/client/petstore/typescript-fetch/builds/allOf-readonly/runtime.ts +++ b/samples/client/petstore/typescript-fetch/builds/allOf-readonly/runtime.ts @@ -346,7 +346,7 @@ function querystringSingleKey(key: string, value: string | number | null | undef return querystringSingleKey(key, valueAsArray, keyPrefix); } if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; } if (value instanceof Object) { return querystring(value as HTTPQuery, fullKey); @@ -359,6 +359,58 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + +export function serializeDate(value: Date): string { + if (isNaN(value.getTime())) { + throw new RangeError('Invalid time value'); + } + // Not padStart: the generated client may target ES6, where it does not exist. + const year = ('000' + value.getFullYear()).slice(-4); + const month = ('0' + (value.getMonth() + 1)).slice(-2); + const day = ('0' + value.getDate()).slice(-2); + return `${year}-${month}-${day}`; +} + + +export function parseDate(value: any): Date { + if (value instanceof Date) { + return value; + } + // `new Date("2026-08-05")` would parse as UTC midnight: a different day west of UTC. + // Local midnight is the stated day everywhere. setFullYear avoids the 1900 offset the + // multi-argument constructor applies to years 0-99. + const fullDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value)); + if (fullDate) { + const year = Number(fullDate[1]); + const month = Number(fullDate[2]) - 1; + const day = Number(fullDate[3]); + const date = new Date(0); + date.setFullYear(year, month, day); + date.setHours(0, 0, 0, 0); + // Out-of-range components (or a day the local zone skipped) silently roll over, + // which would hand back a date the server never sent. + if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { + return new Date(NaN); + } + return date; + } + return new Date(value); +} + +export function parseDateTime(value: any): Date { + return new Date(value); +} + export function mapValues(data: any, fn: (item: any) => any) { const result: { [key: string]: any } = {}; for (const key of Object.keys(data)) { diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-date/.openapi-generator-ignore b/samples/client/petstore/typescript-fetch/builds/date-library-date/.openapi-generator-ignore new file mode 100644 index 000000000000..7484ee590a38 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-date/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-date/.openapi-generator/FILES b/samples/client/petstore/typescript-fetch/builds/date-library-date/.openapi-generator/FILES new file mode 100644 index 000000000000..865383789a36 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-date/.openapi-generator/FILES @@ -0,0 +1,10 @@ +apis/DefaultApi.ts +apis/index.ts +docs/DefaultApi.md +docs/Event.md +docs/Venue.md +index.ts +models/Event.ts +models/Venue.ts +models/index.ts +runtime.ts diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-date/.openapi-generator/VERSION b/samples/client/petstore/typescript-fetch/builds/date-library-date/.openapi-generator/VERSION new file mode 100644 index 000000000000..8fc8df61083a --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-date/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.25.0-SNAPSHOT diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-date/apis/DefaultApi.ts b/samples/client/petstore/typescript-fetch/builds/date-library-date/apis/DefaultApi.ts new file mode 100644 index 000000000000..879209121093 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-date/apis/DefaultApi.ts @@ -0,0 +1,173 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Date handling + * Covers every location a `format: date` or `format: date-time` value can appear in, so the generated (de)serialization can be checked in one place. + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import * as runtime from '../runtime'; +import { + type Event, + EventFromJSON, + EventToJSON, +} from '../models/Event'; + +export interface CreateEventRequest { + /** + * + */ + startsOn: Date; + /** + * + */ + createdAt?: Date; +} + +export interface ListEventsRequest { + /** + * + */ + onDate: Date; + /** + * + */ + from?: Date; + /** + * + */ + updatedSince?: Date; +} + +/** + * + */ +export class DefaultApi extends runtime.BaseAPI { + + /** + * Creates request options for createEvent without sending the request + */ + async createEventRequestOpts(requestParameters: CreateEventRequest): Promise { + if (requestParameters['startsOn'] == null) { + throw new runtime.RequiredError( + 'startsOn', + 'Required parameter "startsOn" was null or undefined when calling createEvent().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + const consumes: runtime.Consume[] = [ + { contentType: 'application/x-www-form-urlencoded' }, + ]; + // @ts-ignore: canConsumeForm may be unused + const canConsumeForm = runtime.canConsumeForm(consumes); + + let formParams: { append(param: string, value: any): any }; + let useForm = false; + if (useForm) { + formParams = new FormData(); + } else { + formParams = new URLSearchParams(); + } + + if (requestParameters['startsOn'] != null) { + formParams.append('startsOn', runtime.serializeDate(requestParameters['startsOn'] as any)); + } + + if (requestParameters['createdAt'] != null) { + formParams.append('createdAt', runtime.serializeDateTime(requestParameters['createdAt'] as any)); + } + + + let urlPath = `/events`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: formParams, + }; + } + + /** + */ + async createEventRaw(requestParameters: CreateEventRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.createEventRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => EventFromJSON(jsonValue)); + } + + /** + */ + async createEvent(requestParameters: CreateEventRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.createEventRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Creates request options for listEvents without sending the request + */ + async listEventsRequestOpts(requestParameters: ListEventsRequest): Promise { + if (requestParameters['onDate'] == null) { + throw new runtime.RequiredError( + 'onDate', + 'Required parameter "onDate" was null or undefined when calling listEvents().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['from'] != null) { + queryParameters['from'] = runtime.serializeDate(requestParameters['from'] as any); + } + + if (requestParameters['updatedSince'] != null) { + queryParameters['updatedSince'] = runtime.serializeDateTime(requestParameters['updatedSince'] as any); + } + + const headerParameters: runtime.HTTPHeaders = {}; + + + let urlPath = `/events/{onDate}`; + if (requestParameters['onDate'] instanceof Date) { + urlPath = urlPath.replace('{onDate}', encodeURIComponent(runtime.serializeDate(requestParameters['onDate']))); + } else { + urlPath = urlPath.replace('{onDate}', encodeURIComponent(String(requestParameters['onDate']))); + } + + return { + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + */ + async listEventsRaw(requestParameters: ListEventsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>> { + const requestOptions = await this.listEventsRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(EventFromJSON)); + } + + /** + */ + async listEvents(requestParameters: ListEventsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const response = await this.listEventsRaw(requestParameters, initOverrides); + return await response.value(); + } + +} diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-date/apis/index.ts b/samples/client/petstore/typescript-fetch/builds/date-library-date/apis/index.ts new file mode 100644 index 000000000000..69c44c00fa0d --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-date/apis/index.ts @@ -0,0 +1,3 @@ +/* tslint:disable */ +/* eslint-disable */ +export * from './DefaultApi'; diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-date/docs/DefaultApi.md b/samples/client/petstore/typescript-fetch/builds/date-library-date/docs/DefaultApi.md new file mode 100644 index 000000000000..a2462430db73 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-date/docs/DefaultApi.md @@ -0,0 +1,149 @@ +# DefaultApi + +All URIs are relative to *http://localhost* + +| Method | HTTP request | Description | +|------------- | ------------- | -------------| +| [**createEvent**](DefaultApi.md#createevent) | **POST** /events | | +| [**listEvents**](DefaultApi.md#listevents) | **GET** /events/{onDate} | | + + + +## createEvent + +> Event createEvent(startsOn, createdAt) + + + +### Example + +```ts +import { + Configuration, + DefaultApi, +} from ''; +import type { CreateEventRequest } from ''; + +async function example() { + console.log("🚀 Testing SDK..."); + const api = new DefaultApi(); + + const body = { + // Date + startsOn: 2013-10-20, + // Date (optional) + createdAt: 2013-10-20T19:20:30+01:00, + } satisfies CreateEventRequest; + + try { + const data = await api.createEvent(body); + console.log(data); + } catch (error) { + console.error(error); + } +} + +// Run the test +example().catch(console.error); +``` + +### Parameters + + +| Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **startsOn** | `Date` | | [Defaults to `undefined`] | +| **createdAt** | `Date` | | [Optional] [Defaults to `undefined`] | + +### Return type + +[**Event**](Event.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: `application/x-www-form-urlencoded` +- **Accept**: `application/json` + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +| **200** | the created event | - | + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + + +## listEvents + +> Array<Event> listEvents(onDate, from, updatedSince) + + + +### Example + +```ts +import { + Configuration, + DefaultApi, +} from ''; +import type { ListEventsRequest } from ''; + +async function example() { + console.log("🚀 Testing SDK..."); + const api = new DefaultApi(); + + const body = { + // Date + onDate: 2013-10-20, + // Date (optional) + from: 2013-10-20, + // Date (optional) + updatedSince: 2013-10-20T19:20:30+01:00, + } satisfies ListEventsRequest; + + try { + const data = await api.listEvents(body); + console.log(data); + } catch (error) { + console.error(error); + } +} + +// Run the test +example().catch(console.error); +``` + +### Parameters + + +| Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **onDate** | `Date` | | [Defaults to `undefined`] | +| **from** | `Date` | | [Optional] [Defaults to `undefined`] | +| **updatedSince** | `Date` | | [Optional] [Defaults to `undefined`] | + +### Return type + +[**Array<Event>**](Event.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: `application/json` + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +| **200** | matching events | - | + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-date/docs/Event.md b/samples/client/petstore/typescript-fetch/builds/date-library-date/docs/Event.md new file mode 100644 index 000000000000..90869c4e773f --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-date/docs/Event.md @@ -0,0 +1,38 @@ + +# Event + + +## Properties + +Name | Type +------------ | ------------- +`startsOn` | Date +`endsOn` | Date +`createdAt` | Date + +## Example + +```typescript +import type { Event } from '' + +// TODO: Update the object below with actual values +const example = { + "startsOn": null, + "endsOn": null, + "createdAt": null, +} satisfies Event + +console.log(example) + +// Convert the instance to a JSON string +const exampleJSON: string = JSON.stringify(example) +console.log(exampleJSON) + +// Parse the JSON string back to an object +const exampleParsed = JSON.parse(exampleJSON) as Event +console.log(exampleParsed) +``` + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + + diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-date/docs/Venue.md b/samples/client/petstore/typescript-fetch/builds/date-library-date/docs/Venue.md new file mode 100644 index 000000000000..9fd3be456ae0 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-date/docs/Venue.md @@ -0,0 +1,35 @@ + +# Venue + +Has no date of any kind, so it must not import the date helpers. + +## Properties + +Name | Type +------------ | ------------- +`name` | string + +## Example + +```typescript +import type { Venue } from '' + +// TODO: Update the object below with actual values +const example = { + "name": null, +} satisfies Venue + +console.log(example) + +// Convert the instance to a JSON string +const exampleJSON: string = JSON.stringify(example) +console.log(exampleJSON) + +// Parse the JSON string back to an object +const exampleParsed = JSON.parse(exampleJSON) as Venue +console.log(exampleParsed) +``` + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + + diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-date/index.ts b/samples/client/petstore/typescript-fetch/builds/date-library-date/index.ts new file mode 100644 index 000000000000..bebe8bbbe206 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-date/index.ts @@ -0,0 +1,5 @@ +/* tslint:disable */ +/* eslint-disable */ +export * from './runtime'; +export * from './apis/index'; +export * from './models/index'; diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-date/models/Event.ts b/samples/client/petstore/typescript-fetch/builds/date-library-date/models/Event.ts new file mode 100644 index 000000000000..6cdf9c513772 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-date/models/Event.ts @@ -0,0 +1,76 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Date handling + * Covers every location a `format: date` or `format: date-time` value can appear in, so the generated (de)serialization can be checked in one place. + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues, parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; +/** + * + * @export + * @interface Event + */ +export interface Event { + /** + * + */ + startsOn: Date; + /** + * + */ + endsOn?: Date | null; + /** + * + */ + createdAt?: Date; +} + +/** + * Check if a given object implements the Event interface. + */ +export function instanceOfEvent(value: object): value is Event { + if (!('startsOn' in value) || value['startsOn'] === undefined) return false; + return true; +} + +export function EventFromJSON(json: any): Event { + return EventFromJSONTyped(json, false); +} + +export function EventFromJSONTyped(json: any, ignoreDiscriminator: boolean): Event { + if (json == null) { + return json; + } + return { + + 'startsOn': (parseDate(json['startsOn'])), + 'endsOn': json['endsOn'] === undefined ? undefined : json['endsOn'] === null ? null : (parseDate(json['endsOn'])), + 'createdAt': json['createdAt'] == null ? undefined : (parseDateTime(json['createdAt'])), + }; +} + +export function EventToJSON(json: any): Event { + return EventToJSONTyped(json, false); +} + +export function EventToJSONTyped(value?: Event | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'startsOn': serializeDate(value['startsOn']), + 'endsOn': value['endsOn'] == null ? value['endsOn'] : serializeDate(value['endsOn']), + 'createdAt': value['createdAt'] == null ? value['createdAt'] : serializeDateTime(value['createdAt']), + }; +} + diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-date/models/Venue.ts b/samples/client/petstore/typescript-fetch/builds/date-library-date/models/Venue.ts new file mode 100644 index 000000000000..1a97a56452f1 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-date/models/Venue.ts @@ -0,0 +1,63 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Date handling + * Covers every location a `format: date` or `format: date-time` value can appear in, so the generated (de)serialization can be checked in one place. + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * Has no date of any kind, so it must not import the date helpers. + * @export + * @interface Venue + */ +export interface Venue { + /** + * + */ + name?: string; +} + +/** + * Check if a given object implements the Venue interface. + */ +export function instanceOfVenue(value: object): value is Venue { + return true; +} + +export function VenueFromJSON(json: any): Venue { + return VenueFromJSONTyped(json, false); +} + +export function VenueFromJSONTyped(json: any, ignoreDiscriminator: boolean): Venue { + if (json == null) { + return json; + } + return { + + 'name': json['name'] == null ? undefined : json['name'], + }; +} + +export function VenueToJSON(json: any): Venue { + return VenueToJSONTyped(json, false); +} + +export function VenueToJSONTyped(value?: Venue | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'name': value['name'], + }; +} + diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-date/models/index.ts b/samples/client/petstore/typescript-fetch/builds/date-library-date/models/index.ts new file mode 100644 index 000000000000..e8e143b24a97 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-date/models/index.ts @@ -0,0 +1,4 @@ +/* tslint:disable */ +/* eslint-disable */ +export * from './Event'; +export * from './Venue'; diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-date/runtime.ts b/samples/client/petstore/typescript-fetch/builds/date-library-date/runtime.ts new file mode 100644 index 000000000000..96f93d3e3f13 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-date/runtime.ts @@ -0,0 +1,506 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Date handling + * Covers every location a `format: date` or `format: date-time` value can appear in, so the generated (de)serialization can be checked in one place. + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +export const BASE_PATH = "http://localhost".replace(/\/+$/, ""); + +export interface ConfigurationParameters { + basePath?: string; // override base path + fetchApi?: FetchAPI; // override for fetch implementation + middleware?: Middleware[]; // middleware to apply before/after fetch requests + queryParamsStringify?: (params: HTTPQuery) => string; // stringify function for query strings + username?: string; // parameter for basic security + password?: string; // parameter for basic security + apiKey?: string | Promise | ((name: string) => string | Promise); // parameter for apiKey security + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string | Promise); // parameter for oauth2 security + headers?: HTTPHeaders; //header params we want to use on every request + credentials?: RequestCredentials; //value for the credentials param we want to use on each request +} + +export class Configuration { + constructor(private configuration: ConfigurationParameters = {}) {} + + set config(configuration: Configuration) { + this.configuration = configuration; + } + + get basePath(): string { + return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH; + } + + get fetchApi(): FetchAPI | undefined { + return this.configuration.fetchApi; + } + + get middleware(): Middleware[] { + return this.configuration.middleware || []; + } + + get queryParamsStringify(): (params: HTTPQuery) => string { + return this.configuration.queryParamsStringify || querystring; + } + + get username(): string | undefined { + return this.configuration.username; + } + + get password(): string | undefined { + return this.configuration.password; + } + + get apiKey(): ((name: string) => string | Promise) | undefined { + const apiKey = this.configuration.apiKey; + if (apiKey) { + return typeof apiKey === 'function' ? apiKey : () => apiKey; + } + return undefined; + } + + get accessToken(): ((name?: string, scopes?: string[]) => string | Promise) | undefined { + const accessToken = this.configuration.accessToken; + if (accessToken) { + return typeof accessToken === 'function' ? accessToken : async () => accessToken; + } + return undefined; + } + + get headers(): HTTPHeaders | undefined { + return this.configuration.headers; + } + + get credentials(): RequestCredentials | undefined { + return this.configuration.credentials; + } +} + +export const DefaultConfig = new Configuration(); + +/** + * This is the base class for all generated API classes. + */ +export class BaseAPI { + + private static readonly jsonRegex = /^(:?application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(:?;.*)?$/i; + private middleware: Middleware[]; + + constructor(protected configuration = DefaultConfig) { + this.middleware = configuration.middleware; + } + + withMiddleware(this: T, ...middlewares: Middleware[]) { + const next = this.clone(); + next.middleware = next.middleware.concat(...middlewares); + return next; + } + + withPreMiddleware(this: T, ...preMiddlewares: Array) { + const middlewares = preMiddlewares.map((pre) => ({ pre })); + return this.withMiddleware(...middlewares); + } + + withPostMiddleware(this: T, ...postMiddlewares: Array) { + const middlewares = postMiddlewares.map((post) => ({ post })); + return this.withMiddleware(...middlewares); + } + + /** + * Check if the given MIME is a JSON MIME. + * JSON MIME examples: + * application/json + * application/json; charset=UTF8 + * APPLICATION/JSON + * application/vnd.company+json + * @param mime - MIME (Multipurpose Internet Mail Extensions) + * @return True if the given MIME is JSON, false otherwise. + */ + protected isJsonMime(mime: string | null | undefined): boolean { + if (!mime) { + return false; + } + return BaseAPI.jsonRegex.test(mime); + } + + protected async request(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction): Promise { + const { url, init } = await this.createFetchParams(context, initOverrides); + const response = await this.fetchApi(url, init); + if (response && (response.status >= 200 && response.status < 300)) { + return response; + } + throw new ResponseError(response, 'Response returned an error code'); + } + + private async createFetchParams(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction) { + let url = this.configuration.basePath + context.path; + if (context.query !== undefined && Object.keys(context.query).length !== 0) { + // only add the querystring to the URL if there are query parameters. + // this is done to avoid urls ending with a "?" character which buggy webservers + // do not handle correctly sometimes. + url += '?' + this.configuration.queryParamsStringify(context.query); + } + + const headers = Object.assign({}, this.configuration.headers, context.headers); + Object.keys(headers).forEach(key => headers[key] === undefined ? delete headers[key] : {}); + + const initOverrideFn = + typeof initOverrides === "function" + ? initOverrides + : async () => initOverrides; + + const initParams = { + method: context.method, + headers, + body: context.body, + credentials: this.configuration.credentials, + }; + + const overriddenInit: RequestInit = { + ...initParams, + ...(await initOverrideFn({ + init: initParams, + context, + })) + }; + + let body: any; + if (isFormData(overriddenInit.body) + || (overriddenInit.body instanceof URLSearchParams) + || isBlob(overriddenInit.body)) { + body = overriddenInit.body; + } else if (this.isJsonMime(headers['Content-Type'])) { + body = JSON.stringify(overriddenInit.body); + } else { + body = overriddenInit.body; + } + + const init: RequestInit = { + ...overriddenInit, + body + }; + + return { url, init }; + } + + private fetchApi = async (url: string, init: RequestInit) => { + let fetchParams = { url, init }; + for (const middleware of this.middleware) { + if (middleware.pre) { + fetchParams = await middleware.pre({ + fetch: this.fetchApi, + ...fetchParams, + }) || fetchParams; + } + } + let response: Response | undefined = undefined; + try { + response = await (this.configuration.fetchApi || fetch)(fetchParams.url, fetchParams.init); + } catch (e) { + for (const middleware of this.middleware) { + if (middleware.onError) { + response = await middleware.onError({ + fetch: this.fetchApi, + url: fetchParams.url, + init: fetchParams.init, + error: e, + response: response ? response.clone() : undefined, + }) || response; + } + } + if (response === undefined) { + if (e instanceof Error) { + throw new FetchError(e, 'The request failed and the interceptors did not return an alternative response'); + } else { + throw e; + } + } + } + for (const middleware of this.middleware) { + if (middleware.post) { + response = await middleware.post({ + fetch: this.fetchApi, + url: fetchParams.url, + init: fetchParams.init, + response: response.clone(), + }) || response; + } + } + return response; + } + + /** + * Create a shallow clone of `this` by constructing a new instance + * and then shallow cloning data members. + */ + private clone(this: T): T { + const constructor = this.constructor as any; + const next = new constructor(this.configuration); + next.middleware = this.middleware.slice(); + return next; + } +}; + +function isBlob(value: any): value is Blob { + return typeof Blob !== 'undefined' && value instanceof Blob; +} + +function isFormData(value: any): value is FormData { + return typeof FormData !== "undefined" && value instanceof FormData; +} + +export class ResponseError extends Error { + override name: "ResponseError" = "ResponseError"; + constructor(public response: Response, msg?: string) { + super(msg); + + // restore prototype chain + const actualProto = new.target.prototype; + if (Object.setPrototypeOf) { + Object.setPrototypeOf(this, actualProto); + } + } +} + +export class FetchError extends Error { + override name: "FetchError" = "FetchError"; + constructor(public cause: Error, msg?: string) { + super(msg); + + // restore prototype chain + const actualProto = new.target.prototype; + if (Object.setPrototypeOf) { + Object.setPrototypeOf(this, actualProto); + } + } +} + +export class RequiredError extends Error { + override name: "RequiredError" = "RequiredError"; + constructor(public field: string, msg?: string) { + super(msg); + + // restore prototype chain + const actualProto = new.target.prototype; + if (Object.setPrototypeOf) { + Object.setPrototypeOf(this, actualProto); + } + } +} + +export const COLLECTION_FORMATS = { + csv: ",", + ssv: " ", + tsv: "\t", + pipes: "|", +}; + +export type FetchAPI = WindowOrWorkerGlobalScope['fetch']; + +export type Json = any; +export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD'; +export type HTTPHeaders = { [key: string]: string }; +export type HTTPQuery = { [key: string]: string | number | null | boolean | Array | Set | HTTPQuery }; +export type HTTPBody = Json | FormData | URLSearchParams; +export type HTTPRequestInit = { headers?: HTTPHeaders; method: HTTPMethod; credentials?: RequestCredentials; body?: HTTPBody }; +export type ModelPropertyNaming = 'camelCase' | 'snake_case' | 'PascalCase' | 'original'; + +export type InitOverrideFunction = (requestContext: { init: HTTPRequestInit, context: RequestOpts }) => Promise + +export interface FetchParams { + url: string; + init: RequestInit; +} + +export interface RequestOpts { + path: string; + method: HTTPMethod; + headers: HTTPHeaders; + query?: HTTPQuery; + body?: HTTPBody; +} + +export function querystring(params: HTTPQuery, prefix: string = ''): string { + return Object.keys(params) + .map(key => querystringSingleKey(key, params[key], prefix)) + .filter(part => part.length > 0) + .join('&'); +} + +function querystringSingleKey(key: string, value: string | number | null | undefined | boolean | Array | Set | HTTPQuery, keyPrefix: string = ''): string { + const fullKey = keyPrefix + (keyPrefix.length ? `[${key}]` : key); + if (value instanceof Array) { + const multiValue = value.map(singleValue => encodeURIComponent(String(singleValue))) + .join(`&${encodeURIComponent(fullKey)}=`); + return `${encodeURIComponent(fullKey)}=${multiValue}`; + } + if (value instanceof Set) { + const valueAsArray = Array.from(value); + return querystringSingleKey(key, valueAsArray, keyPrefix); + } + if (value instanceof Date) { + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; + } + if (value instanceof Object) { + return querystring(value as HTTPQuery, fullKey); + } + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`; +} + +export function exists(json: any, key: string) { + const value = json[key]; + return value !== null && value !== undefined; +} + +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + +export function serializeDate(value: Date): string { + if (isNaN(value.getTime())) { + throw new RangeError('Invalid time value'); + } + // Not padStart: the generated client may target ES6, where it does not exist. + const year = ('000' + value.getFullYear()).slice(-4); + const month = ('0' + (value.getMonth() + 1)).slice(-2); + const day = ('0' + value.getDate()).slice(-2); + return `${year}-${month}-${day}`; +} + + +export function parseDate(value: any): Date { + if (value instanceof Date) { + return value; + } + // `new Date("2026-08-05")` would parse as UTC midnight: a different day west of UTC. + // Local midnight is the stated day everywhere. setFullYear avoids the 1900 offset the + // multi-argument constructor applies to years 0-99. + const fullDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value)); + if (fullDate) { + const year = Number(fullDate[1]); + const month = Number(fullDate[2]) - 1; + const day = Number(fullDate[3]); + const date = new Date(0); + date.setFullYear(year, month, day); + date.setHours(0, 0, 0, 0); + // Out-of-range components (or a day the local zone skipped) silently roll over, + // which would hand back a date the server never sent. + if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { + return new Date(NaN); + } + return date; + } + return new Date(value); +} + +export function parseDateTime(value: any): Date { + return new Date(value); +} + +export function mapValues(data: any, fn: (item: any) => any) { + const result: { [key: string]: any } = {}; + for (const key of Object.keys(data)) { + result[key] = fn(data[key]); + } + return result; +} + +// Pass-through serializer for `any`-typed properties in form data. See #1877. +export function anyToJSON(value: any): any { + return value; +} + +export function canConsumeForm(consumes: Consume[]): boolean { + for (const consume of consumes) { + if (consume.contentType?.startsWith('multipart/form-data') == true) { + return true; + } + } + return false; +} + +export interface Consume { + contentType: string; +} + +export interface RequestContext { + fetch: FetchAPI; + url: string; + init: RequestInit; +} + +export interface ResponseContext { + fetch: FetchAPI; + url: string; + init: RequestInit; + response: Response; +} + +export interface ErrorContext { + fetch: FetchAPI; + url: string; + init: RequestInit; + error: unknown; + response?: Response; +} + +export interface Middleware { + pre?(context: RequestContext): Promise; + post?(context: ResponseContext): Promise; + onError?(context: ErrorContext): Promise; +} + +export interface ApiResponse { + raw: Response; + value(): Promise; +} + +export interface ResponseTransformer { + (json: any): T; +} + +export class JSONApiResponse { + constructor(public raw: Response, private transformer: ResponseTransformer = (jsonValue: any) => jsonValue) {} + + async value(): Promise { + return this.transformer(await this.raw.json()); + } +} + +export class VoidApiResponse { + constructor(public raw: Response) {} + + async value(): Promise { + return undefined; + } +} + +export class BlobApiResponse { + constructor(public raw: Response) {} + + async value(): Promise { + return await this.raw.blob(); + }; +} + +export class TextApiResponse { + constructor(public raw: Response) {} + + async value(): Promise { + return await this.raw.text(); + }; +} diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-string/.openapi-generator-ignore b/samples/client/petstore/typescript-fetch/builds/date-library-string/.openapi-generator-ignore new file mode 100644 index 000000000000..7484ee590a38 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-string/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-string/.openapi-generator/FILES b/samples/client/petstore/typescript-fetch/builds/date-library-string/.openapi-generator/FILES new file mode 100644 index 000000000000..865383789a36 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-string/.openapi-generator/FILES @@ -0,0 +1,10 @@ +apis/DefaultApi.ts +apis/index.ts +docs/DefaultApi.md +docs/Event.md +docs/Venue.md +index.ts +models/Event.ts +models/Venue.ts +models/index.ts +runtime.ts diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-string/.openapi-generator/VERSION b/samples/client/petstore/typescript-fetch/builds/date-library-string/.openapi-generator/VERSION new file mode 100644 index 000000000000..8fc8df61083a --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-string/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.25.0-SNAPSHOT diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-string/apis/DefaultApi.ts b/samples/client/petstore/typescript-fetch/builds/date-library-string/apis/DefaultApi.ts new file mode 100644 index 000000000000..a7a38480f8f9 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-string/apis/DefaultApi.ts @@ -0,0 +1,169 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Date handling + * Covers every location a `format: date` or `format: date-time` value can appear in, so the generated (de)serialization can be checked in one place. + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import * as runtime from '../runtime'; +import { + type Event, + EventFromJSON, + EventToJSON, +} from '../models/Event'; + +export interface CreateEventRequest { + /** + * + */ + startsOn: string; + /** + * + */ + createdAt?: string; +} + +export interface ListEventsRequest { + /** + * + */ + onDate: string; + /** + * + */ + from?: string; + /** + * + */ + updatedSince?: string; +} + +/** + * + */ +export class DefaultApi extends runtime.BaseAPI { + + /** + * Creates request options for createEvent without sending the request + */ + async createEventRequestOpts(requestParameters: CreateEventRequest): Promise { + if (requestParameters['startsOn'] == null) { + throw new runtime.RequiredError( + 'startsOn', + 'Required parameter "startsOn" was null or undefined when calling createEvent().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + const consumes: runtime.Consume[] = [ + { contentType: 'application/x-www-form-urlencoded' }, + ]; + // @ts-ignore: canConsumeForm may be unused + const canConsumeForm = runtime.canConsumeForm(consumes); + + let formParams: { append(param: string, value: any): any }; + let useForm = false; + if (useForm) { + formParams = new FormData(); + } else { + formParams = new URLSearchParams(); + } + + if (requestParameters['startsOn'] != null) { + formParams.append('startsOn', requestParameters['startsOn'] as any); + } + + if (requestParameters['createdAt'] != null) { + formParams.append('createdAt', requestParameters['createdAt'] as any); + } + + + let urlPath = `/events`; + + return { + path: urlPath, + method: 'POST', + headers: headerParameters, + query: queryParameters, + body: formParams, + }; + } + + /** + */ + async createEventRaw(requestParameters: CreateEventRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const requestOptions = await this.createEventRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => EventFromJSON(jsonValue)); + } + + /** + */ + async createEvent(requestParameters: CreateEventRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.createEventRaw(requestParameters, initOverrides); + return await response.value(); + } + + /** + * Creates request options for listEvents without sending the request + */ + async listEventsRequestOpts(requestParameters: ListEventsRequest): Promise { + if (requestParameters['onDate'] == null) { + throw new runtime.RequiredError( + 'onDate', + 'Required parameter "onDate" was null or undefined when calling listEvents().' + ); + } + + const queryParameters: any = {}; + + if (requestParameters['from'] != null) { + queryParameters['from'] = requestParameters['from']; + } + + if (requestParameters['updatedSince'] != null) { + queryParameters['updatedSince'] = requestParameters['updatedSince']; + } + + const headerParameters: runtime.HTTPHeaders = {}; + + + let urlPath = `/events/{onDate}`; + urlPath = urlPath.replace('{onDate}', encodeURIComponent(String(requestParameters['onDate']))); + + return { + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }; + } + + /** + */ + async listEventsRaw(requestParameters: ListEventsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>> { + const requestOptions = await this.listEventsRequestOpts(requestParameters); + const response = await this.request(requestOptions, initOverrides); + + return new runtime.JSONApiResponse(response, (jsonValue) => jsonValue.map(EventFromJSON)); + } + + /** + */ + async listEvents(requestParameters: ListEventsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + const response = await this.listEventsRaw(requestParameters, initOverrides); + return await response.value(); + } + +} diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-string/apis/index.ts b/samples/client/petstore/typescript-fetch/builds/date-library-string/apis/index.ts new file mode 100644 index 000000000000..69c44c00fa0d --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-string/apis/index.ts @@ -0,0 +1,3 @@ +/* tslint:disable */ +/* eslint-disable */ +export * from './DefaultApi'; diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-string/docs/DefaultApi.md b/samples/client/petstore/typescript-fetch/builds/date-library-string/docs/DefaultApi.md new file mode 100644 index 000000000000..d853cc5ad399 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-string/docs/DefaultApi.md @@ -0,0 +1,149 @@ +# DefaultApi + +All URIs are relative to *http://localhost* + +| Method | HTTP request | Description | +|------------- | ------------- | -------------| +| [**createEvent**](DefaultApi.md#createevent) | **POST** /events | | +| [**listEvents**](DefaultApi.md#listevents) | **GET** /events/{onDate} | | + + + +## createEvent + +> Event createEvent(startsOn, createdAt) + + + +### Example + +```ts +import { + Configuration, + DefaultApi, +} from ''; +import type { CreateEventRequest } from ''; + +async function example() { + console.log("🚀 Testing SDK..."); + const api = new DefaultApi(); + + const body = { + // string + startsOn: 2013-10-20, + // string (optional) + createdAt: 2013-10-20T19:20:30+01:00, + } satisfies CreateEventRequest; + + try { + const data = await api.createEvent(body); + console.log(data); + } catch (error) { + console.error(error); + } +} + +// Run the test +example().catch(console.error); +``` + +### Parameters + + +| Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **startsOn** | `string` | | [Defaults to `undefined`] | +| **createdAt** | `string` | | [Optional] [Defaults to `undefined`] | + +### Return type + +[**Event**](Event.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: `application/x-www-form-urlencoded` +- **Accept**: `application/json` + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +| **200** | the created event | - | + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + + +## listEvents + +> Array<Event> listEvents(onDate, from, updatedSince) + + + +### Example + +```ts +import { + Configuration, + DefaultApi, +} from ''; +import type { ListEventsRequest } from ''; + +async function example() { + console.log("🚀 Testing SDK..."); + const api = new DefaultApi(); + + const body = { + // string + onDate: 2013-10-20, + // string (optional) + from: 2013-10-20, + // string (optional) + updatedSince: 2013-10-20T19:20:30+01:00, + } satisfies ListEventsRequest; + + try { + const data = await api.listEvents(body); + console.log(data); + } catch (error) { + console.error(error); + } +} + +// Run the test +example().catch(console.error); +``` + +### Parameters + + +| Name | Type | Description | Notes | +|------------- | ------------- | ------------- | -------------| +| **onDate** | `string` | | [Defaults to `undefined`] | +| **from** | `string` | | [Optional] [Defaults to `undefined`] | +| **updatedSince** | `string` | | [Optional] [Defaults to `undefined`] | + +### Return type + +[**Array<Event>**](Event.md) + +### Authorization + +No authorization required + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: `application/json` + + +### HTTP response details +| Status code | Description | Response headers | +|-------------|-------------|------------------| +| **200** | matching events | - | + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-string/docs/Event.md b/samples/client/petstore/typescript-fetch/builds/date-library-string/docs/Event.md new file mode 100644 index 000000000000..4e009ab2a252 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-string/docs/Event.md @@ -0,0 +1,38 @@ + +# Event + + +## Properties + +Name | Type +------------ | ------------- +`startsOn` | string +`endsOn` | string +`createdAt` | string + +## Example + +```typescript +import type { Event } from '' + +// TODO: Update the object below with actual values +const example = { + "startsOn": null, + "endsOn": null, + "createdAt": null, +} satisfies Event + +console.log(example) + +// Convert the instance to a JSON string +const exampleJSON: string = JSON.stringify(example) +console.log(exampleJSON) + +// Parse the JSON string back to an object +const exampleParsed = JSON.parse(exampleJSON) as Event +console.log(exampleParsed) +``` + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + + diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-string/docs/Venue.md b/samples/client/petstore/typescript-fetch/builds/date-library-string/docs/Venue.md new file mode 100644 index 000000000000..9fd3be456ae0 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-string/docs/Venue.md @@ -0,0 +1,35 @@ + +# Venue + +Has no date of any kind, so it must not import the date helpers. + +## Properties + +Name | Type +------------ | ------------- +`name` | string + +## Example + +```typescript +import type { Venue } from '' + +// TODO: Update the object below with actual values +const example = { + "name": null, +} satisfies Venue + +console.log(example) + +// Convert the instance to a JSON string +const exampleJSON: string = JSON.stringify(example) +console.log(exampleJSON) + +// Parse the JSON string back to an object +const exampleParsed = JSON.parse(exampleJSON) as Venue +console.log(exampleParsed) +``` + +[[Back to top]](#) [[Back to API list]](../README.md#api-endpoints) [[Back to Model list]](../README.md#models) [[Back to README]](../README.md) + + diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-string/index.ts b/samples/client/petstore/typescript-fetch/builds/date-library-string/index.ts new file mode 100644 index 000000000000..bebe8bbbe206 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-string/index.ts @@ -0,0 +1,5 @@ +/* tslint:disable */ +/* eslint-disable */ +export * from './runtime'; +export * from './apis/index'; +export * from './models/index'; diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-string/models/Event.ts b/samples/client/petstore/typescript-fetch/builds/date-library-string/models/Event.ts new file mode 100644 index 000000000000..1a9bd5a58641 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-string/models/Event.ts @@ -0,0 +1,76 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Date handling + * Covers every location a `format: date` or `format: date-time` value can appear in, so the generated (de)serialization can be checked in one place. + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface Event + */ +export interface Event { + /** + * + */ + startsOn: string; + /** + * + */ + endsOn?: string | null; + /** + * + */ + createdAt?: string; +} + +/** + * Check if a given object implements the Event interface. + */ +export function instanceOfEvent(value: object): value is Event { + if (!('startsOn' in value) || value['startsOn'] === undefined) return false; + return true; +} + +export function EventFromJSON(json: any): Event { + return EventFromJSONTyped(json, false); +} + +export function EventFromJSONTyped(json: any, ignoreDiscriminator: boolean): Event { + if (json == null) { + return json; + } + return { + + 'startsOn': json['startsOn'], + 'endsOn': json['endsOn'] === undefined ? undefined : json['endsOn'] === null ? null : json['endsOn'], + 'createdAt': json['createdAt'] == null ? undefined : json['createdAt'], + }; +} + +export function EventToJSON(json: any): Event { + return EventToJSONTyped(json, false); +} + +export function EventToJSONTyped(value?: Event | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'startsOn': value['startsOn'], + 'endsOn': value['endsOn'], + 'createdAt': value['createdAt'], + }; +} + diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-string/models/Venue.ts b/samples/client/petstore/typescript-fetch/builds/date-library-string/models/Venue.ts new file mode 100644 index 000000000000..1a97a56452f1 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-string/models/Venue.ts @@ -0,0 +1,63 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Date handling + * Covers every location a `format: date` or `format: date-time` value can appear in, so the generated (de)serialization can be checked in one place. + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * Has no date of any kind, so it must not import the date helpers. + * @export + * @interface Venue + */ +export interface Venue { + /** + * + */ + name?: string; +} + +/** + * Check if a given object implements the Venue interface. + */ +export function instanceOfVenue(value: object): value is Venue { + return true; +} + +export function VenueFromJSON(json: any): Venue { + return VenueFromJSONTyped(json, false); +} + +export function VenueFromJSONTyped(json: any, ignoreDiscriminator: boolean): Venue { + if (json == null) { + return json; + } + return { + + 'name': json['name'] == null ? undefined : json['name'], + }; +} + +export function VenueToJSON(json: any): Venue { + return VenueToJSONTyped(json, false); +} + +export function VenueToJSONTyped(value?: Venue | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'name': value['name'], + }; +} + diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-string/models/index.ts b/samples/client/petstore/typescript-fetch/builds/date-library-string/models/index.ts new file mode 100644 index 000000000000..e8e143b24a97 --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-string/models/index.ts @@ -0,0 +1,4 @@ +/* tslint:disable */ +/* eslint-disable */ +export * from './Event'; +export * from './Venue'; diff --git a/samples/client/petstore/typescript-fetch/builds/date-library-string/runtime.ts b/samples/client/petstore/typescript-fetch/builds/date-library-string/runtime.ts new file mode 100644 index 000000000000..2535518eb0ca --- /dev/null +++ b/samples/client/petstore/typescript-fetch/builds/date-library-string/runtime.ts @@ -0,0 +1,466 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Date handling + * Covers every location a `format: date` or `format: date-time` value can appear in, so the generated (de)serialization can be checked in one place. + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +export const BASE_PATH = "http://localhost".replace(/\/+$/, ""); + +export interface ConfigurationParameters { + basePath?: string; // override base path + fetchApi?: FetchAPI; // override for fetch implementation + middleware?: Middleware[]; // middleware to apply before/after fetch requests + queryParamsStringify?: (params: HTTPQuery) => string; // stringify function for query strings + username?: string; // parameter for basic security + password?: string; // parameter for basic security + apiKey?: string | Promise | ((name: string) => string | Promise); // parameter for apiKey security + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string | Promise); // parameter for oauth2 security + headers?: HTTPHeaders; //header params we want to use on every request + credentials?: RequestCredentials; //value for the credentials param we want to use on each request +} + +export class Configuration { + constructor(private configuration: ConfigurationParameters = {}) {} + + set config(configuration: Configuration) { + this.configuration = configuration; + } + + get basePath(): string { + return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH; + } + + get fetchApi(): FetchAPI | undefined { + return this.configuration.fetchApi; + } + + get middleware(): Middleware[] { + return this.configuration.middleware || []; + } + + get queryParamsStringify(): (params: HTTPQuery) => string { + return this.configuration.queryParamsStringify || querystring; + } + + get username(): string | undefined { + return this.configuration.username; + } + + get password(): string | undefined { + return this.configuration.password; + } + + get apiKey(): ((name: string) => string | Promise) | undefined { + const apiKey = this.configuration.apiKey; + if (apiKey) { + return typeof apiKey === 'function' ? apiKey : () => apiKey; + } + return undefined; + } + + get accessToken(): ((name?: string, scopes?: string[]) => string | Promise) | undefined { + const accessToken = this.configuration.accessToken; + if (accessToken) { + return typeof accessToken === 'function' ? accessToken : async () => accessToken; + } + return undefined; + } + + get headers(): HTTPHeaders | undefined { + return this.configuration.headers; + } + + get credentials(): RequestCredentials | undefined { + return this.configuration.credentials; + } +} + +export const DefaultConfig = new Configuration(); + +/** + * This is the base class for all generated API classes. + */ +export class BaseAPI { + + private static readonly jsonRegex = /^(:?application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(:?;.*)?$/i; + private middleware: Middleware[]; + + constructor(protected configuration = DefaultConfig) { + this.middleware = configuration.middleware; + } + + withMiddleware(this: T, ...middlewares: Middleware[]) { + const next = this.clone(); + next.middleware = next.middleware.concat(...middlewares); + return next; + } + + withPreMiddleware(this: T, ...preMiddlewares: Array) { + const middlewares = preMiddlewares.map((pre) => ({ pre })); + return this.withMiddleware(...middlewares); + } + + withPostMiddleware(this: T, ...postMiddlewares: Array) { + const middlewares = postMiddlewares.map((post) => ({ post })); + return this.withMiddleware(...middlewares); + } + + /** + * Check if the given MIME is a JSON MIME. + * JSON MIME examples: + * application/json + * application/json; charset=UTF8 + * APPLICATION/JSON + * application/vnd.company+json + * @param mime - MIME (Multipurpose Internet Mail Extensions) + * @return True if the given MIME is JSON, false otherwise. + */ + protected isJsonMime(mime: string | null | undefined): boolean { + if (!mime) { + return false; + } + return BaseAPI.jsonRegex.test(mime); + } + + protected async request(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction): Promise { + const { url, init } = await this.createFetchParams(context, initOverrides); + const response = await this.fetchApi(url, init); + if (response && (response.status >= 200 && response.status < 300)) { + return response; + } + throw new ResponseError(response, 'Response returned an error code'); + } + + private async createFetchParams(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction) { + let url = this.configuration.basePath + context.path; + if (context.query !== undefined && Object.keys(context.query).length !== 0) { + // only add the querystring to the URL if there are query parameters. + // this is done to avoid urls ending with a "?" character which buggy webservers + // do not handle correctly sometimes. + url += '?' + this.configuration.queryParamsStringify(context.query); + } + + const headers = Object.assign({}, this.configuration.headers, context.headers); + Object.keys(headers).forEach(key => headers[key] === undefined ? delete headers[key] : {}); + + const initOverrideFn = + typeof initOverrides === "function" + ? initOverrides + : async () => initOverrides; + + const initParams = { + method: context.method, + headers, + body: context.body, + credentials: this.configuration.credentials, + }; + + const overriddenInit: RequestInit = { + ...initParams, + ...(await initOverrideFn({ + init: initParams, + context, + })) + }; + + let body: any; + if (isFormData(overriddenInit.body) + || (overriddenInit.body instanceof URLSearchParams) + || isBlob(overriddenInit.body)) { + body = overriddenInit.body; + } else if (this.isJsonMime(headers['Content-Type'])) { + body = JSON.stringify(overriddenInit.body); + } else { + body = overriddenInit.body; + } + + const init: RequestInit = { + ...overriddenInit, + body + }; + + return { url, init }; + } + + private fetchApi = async (url: string, init: RequestInit) => { + let fetchParams = { url, init }; + for (const middleware of this.middleware) { + if (middleware.pre) { + fetchParams = await middleware.pre({ + fetch: this.fetchApi, + ...fetchParams, + }) || fetchParams; + } + } + let response: Response | undefined = undefined; + try { + response = await (this.configuration.fetchApi || fetch)(fetchParams.url, fetchParams.init); + } catch (e) { + for (const middleware of this.middleware) { + if (middleware.onError) { + response = await middleware.onError({ + fetch: this.fetchApi, + url: fetchParams.url, + init: fetchParams.init, + error: e, + response: response ? response.clone() : undefined, + }) || response; + } + } + if (response === undefined) { + if (e instanceof Error) { + throw new FetchError(e, 'The request failed and the interceptors did not return an alternative response'); + } else { + throw e; + } + } + } + for (const middleware of this.middleware) { + if (middleware.post) { + response = await middleware.post({ + fetch: this.fetchApi, + url: fetchParams.url, + init: fetchParams.init, + response: response.clone(), + }) || response; + } + } + return response; + } + + /** + * Create a shallow clone of `this` by constructing a new instance + * and then shallow cloning data members. + */ + private clone(this: T): T { + const constructor = this.constructor as any; + const next = new constructor(this.configuration); + next.middleware = this.middleware.slice(); + return next; + } +}; + +function isBlob(value: any): value is Blob { + return typeof Blob !== 'undefined' && value instanceof Blob; +} + +function isFormData(value: any): value is FormData { + return typeof FormData !== "undefined" && value instanceof FormData; +} + +export class ResponseError extends Error { + override name: "ResponseError" = "ResponseError"; + constructor(public response: Response, msg?: string) { + super(msg); + + // restore prototype chain + const actualProto = new.target.prototype; + if (Object.setPrototypeOf) { + Object.setPrototypeOf(this, actualProto); + } + } +} + +export class FetchError extends Error { + override name: "FetchError" = "FetchError"; + constructor(public cause: Error, msg?: string) { + super(msg); + + // restore prototype chain + const actualProto = new.target.prototype; + if (Object.setPrototypeOf) { + Object.setPrototypeOf(this, actualProto); + } + } +} + +export class RequiredError extends Error { + override name: "RequiredError" = "RequiredError"; + constructor(public field: string, msg?: string) { + super(msg); + + // restore prototype chain + const actualProto = new.target.prototype; + if (Object.setPrototypeOf) { + Object.setPrototypeOf(this, actualProto); + } + } +} + +export const COLLECTION_FORMATS = { + csv: ",", + ssv: " ", + tsv: "\t", + pipes: "|", +}; + +export type FetchAPI = WindowOrWorkerGlobalScope['fetch']; + +export type Json = any; +export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD'; +export type HTTPHeaders = { [key: string]: string }; +export type HTTPQuery = { [key: string]: string | number | null | boolean | Array | Set | HTTPQuery }; +export type HTTPBody = Json | FormData | URLSearchParams; +export type HTTPRequestInit = { headers?: HTTPHeaders; method: HTTPMethod; credentials?: RequestCredentials; body?: HTTPBody }; +export type ModelPropertyNaming = 'camelCase' | 'snake_case' | 'PascalCase' | 'original'; + +export type InitOverrideFunction = (requestContext: { init: HTTPRequestInit, context: RequestOpts }) => Promise + +export interface FetchParams { + url: string; + init: RequestInit; +} + +export interface RequestOpts { + path: string; + method: HTTPMethod; + headers: HTTPHeaders; + query?: HTTPQuery; + body?: HTTPBody; +} + +export function querystring(params: HTTPQuery, prefix: string = ''): string { + return Object.keys(params) + .map(key => querystringSingleKey(key, params[key], prefix)) + .filter(part => part.length > 0) + .join('&'); +} + +function querystringSingleKey(key: string, value: string | number | null | undefined | boolean | Array | Set | HTTPQuery, keyPrefix: string = ''): string { + const fullKey = keyPrefix + (keyPrefix.length ? `[${key}]` : key); + if (value instanceof Array) { + const multiValue = value.map(singleValue => encodeURIComponent(String(singleValue))) + .join(`&${encodeURIComponent(fullKey)}=`); + return `${encodeURIComponent(fullKey)}=${multiValue}`; + } + if (value instanceof Set) { + const valueAsArray = Array.from(value); + return querystringSingleKey(key, valueAsArray, keyPrefix); + } + if (value instanceof Date) { + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; + } + if (value instanceof Object) { + return querystring(value as HTTPQuery, fullKey); + } + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`; +} + +export function exists(json: any, key: string) { + const value = json[key]; + return value !== null && value !== undefined; +} + +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + + +export function mapValues(data: any, fn: (item: any) => any) { + const result: { [key: string]: any } = {}; + for (const key of Object.keys(data)) { + result[key] = fn(data[key]); + } + return result; +} + +// Pass-through serializer for `any`-typed properties in form data. See #1877. +export function anyToJSON(value: any): any { + return value; +} + +export function canConsumeForm(consumes: Consume[]): boolean { + for (const consume of consumes) { + if (consume.contentType?.startsWith('multipart/form-data') == true) { + return true; + } + } + return false; +} + +export interface Consume { + contentType: string; +} + +export interface RequestContext { + fetch: FetchAPI; + url: string; + init: RequestInit; +} + +export interface ResponseContext { + fetch: FetchAPI; + url: string; + init: RequestInit; + response: Response; +} + +export interface ErrorContext { + fetch: FetchAPI; + url: string; + init: RequestInit; + error: unknown; + response?: Response; +} + +export interface Middleware { + pre?(context: RequestContext): Promise; + post?(context: ResponseContext): Promise; + onError?(context: ErrorContext): Promise; +} + +export interface ApiResponse { + raw: Response; + value(): Promise; +} + +export interface ResponseTransformer { + (json: any): T; +} + +export class JSONApiResponse { + constructor(public raw: Response, private transformer: ResponseTransformer = (jsonValue: any) => jsonValue) {} + + async value(): Promise { + return this.transformer(await this.raw.json()); + } +} + +export class VoidApiResponse { + constructor(public raw: Response) {} + + async value(): Promise { + return undefined; + } +} + +export class BlobApiResponse { + constructor(public raw: Response) {} + + async value(): Promise { + return await this.raw.blob(); + }; +} + +export class TextApiResponse { + constructor(public raw: Response) {} + + async value(): Promise { + return await this.raw.text(); + }; +} diff --git a/samples/client/petstore/typescript-fetch/builds/default-v3.0/apis/FakeApi.ts b/samples/client/petstore/typescript-fetch/builds/default-v3.0/apis/FakeApi.ts index e54629f53eca..26b4e14eb660 100644 --- a/samples/client/petstore/typescript-fetch/builds/default-v3.0/apis/FakeApi.ts +++ b/samples/client/petstore/typescript-fetch/builds/default-v3.0/apis/FakeApi.ts @@ -1045,11 +1045,11 @@ export class FakeApi extends runtime.BaseAPI { } if (requestParameters['date'] != null) { - formParams.append('date', requestParameters['date'] as any); + formParams.append('date', runtime.serializeDate(requestParameters['date'] as any)); } if (requestParameters['dateTime'] != null) { - formParams.append('dateTime', (requestParameters['dateTime'] as any).toISOString()); + formParams.append('dateTime', runtime.serializeDateTime(requestParameters['dateTime'] as any)); } if (requestParameters['password'] != null) { diff --git a/samples/client/petstore/typescript-fetch/builds/default-v3.0/models/FormatTest.ts b/samples/client/petstore/typescript-fetch/builds/default-v3.0/models/FormatTest.ts index 047bde78c359..aa1f8c7f1518 100644 --- a/samples/client/petstore/typescript-fetch/builds/default-v3.0/models/FormatTest.ts +++ b/samples/client/petstore/typescript-fetch/builds/default-v3.0/models/FormatTest.ts @@ -12,7 +12,7 @@ * Do not edit the class manually. */ -import { mapValues } from '../runtime'; +import { mapValues, parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; /** * * @export @@ -116,8 +116,8 @@ export function FormatTestFromJSONTyped(json: any, ignoreDiscriminator: boolean) 'string': json['string'] == null ? undefined : json['string'], '_byte': json['byte'], 'binary': json['binary'] == null ? undefined : json['binary'], - 'date': (new Date(json['date'])), - 'dateTime': json['dateTime'] == null ? undefined : (new Date(json['dateTime'])), + 'date': (parseDate(json['date'])), + 'dateTime': json['dateTime'] == null ? undefined : (parseDateTime(json['dateTime'])), 'uuid': json['uuid'] == null ? undefined : json['uuid'], 'password': json['password'], 'patternWithDigits': json['pattern_with_digits'] == null ? undefined : json['pattern_with_digits'], @@ -146,8 +146,8 @@ export function FormatTestToJSONTyped(value?: FormatTest | null, ignoreDiscrimin 'string': value['string'], 'byte': value['_byte'], 'binary': value['binary'], - 'date': value['date'].toISOString().substring(0,10), - 'dateTime': value['dateTime'] == null ? value['dateTime'] : value['dateTime'].toISOString(), + 'date': serializeDate(value['date']), + 'dateTime': value['dateTime'] == null ? value['dateTime'] : serializeDateTime(value['dateTime']), 'uuid': value['uuid'], 'password': value['password'], 'pattern_with_digits': value['patternWithDigits'], diff --git a/samples/client/petstore/typescript-fetch/builds/default-v3.0/models/MixedPropertiesAndAdditionalPropertiesClass.ts b/samples/client/petstore/typescript-fetch/builds/default-v3.0/models/MixedPropertiesAndAdditionalPropertiesClass.ts index cb7a2989f0c9..29e136055639 100644 --- a/samples/client/petstore/typescript-fetch/builds/default-v3.0/models/MixedPropertiesAndAdditionalPropertiesClass.ts +++ b/samples/client/petstore/typescript-fetch/builds/default-v3.0/models/MixedPropertiesAndAdditionalPropertiesClass.ts @@ -12,7 +12,7 @@ * Do not edit the class manually. */ -import { mapValues } from '../runtime'; +import { mapValues, parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; import type { Animal } from './Animal'; import { AnimalFromJSON, @@ -59,7 +59,7 @@ export function MixedPropertiesAndAdditionalPropertiesClassFromJSONTyped(json: a return { 'uuid': json['uuid'] == null ? undefined : json['uuid'], - 'dateTime': json['dateTime'] == null ? undefined : (new Date(json['dateTime'])), + 'dateTime': json['dateTime'] == null ? undefined : (parseDateTime(json['dateTime'])), 'map': json['map'] == null ? undefined : (mapValues(json['map'], AnimalFromJSON)), }; } @@ -76,7 +76,7 @@ export function MixedPropertiesAndAdditionalPropertiesClassToJSONTyped(value?: M return { 'uuid': value['uuid'], - 'dateTime': value['dateTime'] == null ? value['dateTime'] : value['dateTime'].toISOString(), + 'dateTime': value['dateTime'] == null ? value['dateTime'] : serializeDateTime(value['dateTime']), 'map': value['map'] == null ? undefined : (mapValues(value['map'], AnimalToJSON)), }; } diff --git a/samples/client/petstore/typescript-fetch/builds/default-v3.0/models/NullableClass.ts b/samples/client/petstore/typescript-fetch/builds/default-v3.0/models/NullableClass.ts index 2899b9f6f3d6..e917dc5109d3 100644 --- a/samples/client/petstore/typescript-fetch/builds/default-v3.0/models/NullableClass.ts +++ b/samples/client/petstore/typescript-fetch/builds/default-v3.0/models/NullableClass.ts @@ -12,7 +12,7 @@ * Do not edit the class manually. */ -import { mapValues } from '../runtime'; +import { mapValues, parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; /** * * @export @@ -92,8 +92,8 @@ export function NullableClassFromJSONTyped(json: any, ignoreDiscriminator: boole 'numberProp': json['number_prop'] === undefined ? undefined : json['number_prop'] === null ? null : json['number_prop'], 'booleanProp': json['boolean_prop'] === undefined ? undefined : json['boolean_prop'] === null ? null : json['boolean_prop'], 'stringProp': json['string_prop'] === undefined ? undefined : json['string_prop'] === null ? null : json['string_prop'], - 'dateProp': json['date_prop'] === undefined ? undefined : json['date_prop'] === null ? null : (new Date(json['date_prop'])), - 'datetimeProp': json['datetime_prop'] === undefined ? undefined : json['datetime_prop'] === null ? null : (new Date(json['datetime_prop'])), + 'dateProp': json['date_prop'] === undefined ? undefined : json['date_prop'] === null ? null : (parseDate(json['date_prop'])), + 'datetimeProp': json['datetime_prop'] === undefined ? undefined : json['datetime_prop'] === null ? null : (parseDateTime(json['datetime_prop'])), 'arrayNullableProp': json['array_nullable_prop'] === undefined ? undefined : json['array_nullable_prop'] === null ? null : json['array_nullable_prop'], 'arrayAndItemsNullableProp': json['array_and_items_nullable_prop'] === undefined ? undefined : json['array_and_items_nullable_prop'] === null ? null : json['array_and_items_nullable_prop'], 'arrayItemsNullable': json['array_items_nullable'] == null ? undefined : json['array_items_nullable'], @@ -119,8 +119,8 @@ export function NullableClassToJSONTyped(value?: NullableClass | null, ignoreDis 'number_prop': value['numberProp'], 'boolean_prop': value['booleanProp'], 'string_prop': value['stringProp'], - 'date_prop': value['dateProp'] == null ? value['dateProp'] : value['dateProp'].toISOString().substring(0,10), - 'datetime_prop': value['datetimeProp'] == null ? value['datetimeProp'] : value['datetimeProp'].toISOString(), + 'date_prop': value['dateProp'] == null ? value['dateProp'] : serializeDate(value['dateProp']), + 'datetime_prop': value['datetimeProp'] == null ? value['datetimeProp'] : serializeDateTime(value['datetimeProp']), 'array_nullable_prop': value['arrayNullableProp'], 'array_and_items_nullable_prop': value['arrayAndItemsNullableProp'], 'array_items_nullable': value['arrayItemsNullable'], diff --git a/samples/client/petstore/typescript-fetch/builds/default-v3.0/models/Order.ts b/samples/client/petstore/typescript-fetch/builds/default-v3.0/models/Order.ts index 1d32da2609f6..8cb7d0bb68bb 100644 --- a/samples/client/petstore/typescript-fetch/builds/default-v3.0/models/Order.ts +++ b/samples/client/petstore/typescript-fetch/builds/default-v3.0/models/Order.ts @@ -12,7 +12,7 @@ * Do not edit the class manually. */ -import { mapValues } from '../runtime'; +import { mapValues, parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; /** * * @export @@ -77,7 +77,7 @@ export function OrderFromJSONTyped(json: any, ignoreDiscriminator: boolean): Ord 'id': json['id'] == null ? undefined : json['id'], 'petId': json['petId'] == null ? undefined : json['petId'], 'quantity': json['quantity'] == null ? undefined : json['quantity'], - 'shipDate': json['shipDate'] == null ? undefined : (new Date(json['shipDate'])), + 'shipDate': json['shipDate'] == null ? undefined : (parseDateTime(json['shipDate'])), 'status': json['status'] == null ? undefined : json['status'], 'complete': json['complete'] == null ? undefined : json['complete'], }; @@ -97,7 +97,7 @@ export function OrderToJSONTyped(value?: Order | null, ignoreDiscriminator: bool 'id': value['id'], 'petId': value['petId'], 'quantity': value['quantity'], - 'shipDate': value['shipDate'] == null ? value['shipDate'] : value['shipDate'].toISOString(), + 'shipDate': value['shipDate'] == null ? value['shipDate'] : serializeDateTime(value['shipDate']), 'status': value['status'], 'complete': value['complete'], }; diff --git a/samples/client/petstore/typescript-fetch/builds/default-v3.0/runtime.ts b/samples/client/petstore/typescript-fetch/builds/default-v3.0/runtime.ts index e52a36947b03..5b823ec12c0d 100644 --- a/samples/client/petstore/typescript-fetch/builds/default-v3.0/runtime.ts +++ b/samples/client/petstore/typescript-fetch/builds/default-v3.0/runtime.ts @@ -346,7 +346,7 @@ function querystringSingleKey(key: string, value: string | number | null | undef return querystringSingleKey(key, valueAsArray, keyPrefix); } if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; } if (value instanceof Object) { return querystring(value as HTTPQuery, fullKey); @@ -359,6 +359,58 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + +export function serializeDate(value: Date): string { + if (isNaN(value.getTime())) { + throw new RangeError('Invalid time value'); + } + // Not padStart: the generated client may target ES6, where it does not exist. + const year = ('000' + value.getFullYear()).slice(-4); + const month = ('0' + (value.getMonth() + 1)).slice(-2); + const day = ('0' + value.getDate()).slice(-2); + return `${year}-${month}-${day}`; +} + + +export function parseDate(value: any): Date { + if (value instanceof Date) { + return value; + } + // `new Date("2026-08-05")` would parse as UTC midnight: a different day west of UTC. + // Local midnight is the stated day everywhere. setFullYear avoids the 1900 offset the + // multi-argument constructor applies to years 0-99. + const fullDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value)); + if (fullDate) { + const year = Number(fullDate[1]); + const month = Number(fullDate[2]) - 1; + const day = Number(fullDate[3]); + const date = new Date(0); + date.setFullYear(year, month, day); + date.setHours(0, 0, 0, 0); + // Out-of-range components (or a day the local zone skipped) silently roll over, + // which would hand back a date the server never sent. + if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { + return new Date(NaN); + } + return date; + } + return new Date(value); +} + +export function parseDateTime(value: any): Date { + return new Date(value); +} + export function mapValues(data: any, fn: (item: any) => any) { const result: { [key: string]: any } = {}; for (const key of Object.keys(data)) { diff --git a/samples/client/petstore/typescript-fetch/builds/default/models/Order.ts b/samples/client/petstore/typescript-fetch/builds/default/models/Order.ts index cb6b5c016de6..e47ffdb65be7 100644 --- a/samples/client/petstore/typescript-fetch/builds/default/models/Order.ts +++ b/samples/client/petstore/typescript-fetch/builds/default/models/Order.ts @@ -12,7 +12,7 @@ * Do not edit the class manually. */ -import { mapValues } from '../runtime'; +import { mapValues, parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; /** * An order for a pets from the pet store * @export @@ -77,7 +77,7 @@ export function OrderFromJSONTyped(json: any, ignoreDiscriminator: boolean): Ord 'id': json['id'] == null ? undefined : json['id'], 'petId': json['petId'] == null ? undefined : json['petId'], 'quantity': json['quantity'] == null ? undefined : json['quantity'], - 'shipDate': json['shipDate'] == null ? undefined : (new Date(json['shipDate'])), + 'shipDate': json['shipDate'] == null ? undefined : (parseDateTime(json['shipDate'])), 'status': json['status'] == null ? undefined : json['status'], 'complete': json['complete'] == null ? undefined : json['complete'], }; @@ -97,7 +97,7 @@ export function OrderToJSONTyped(value?: Order | null, ignoreDiscriminator: bool 'id': value['id'], 'petId': value['petId'], 'quantity': value['quantity'], - 'shipDate': value['shipDate'] == null ? value['shipDate'] : value['shipDate'].toISOString(), + 'shipDate': value['shipDate'] == null ? value['shipDate'] : serializeDateTime(value['shipDate']), 'status': value['status'], 'complete': value['complete'], }; diff --git a/samples/client/petstore/typescript-fetch/builds/default/runtime.ts b/samples/client/petstore/typescript-fetch/builds/default/runtime.ts index 3389e4beb1cc..4ea4c5de5589 100644 --- a/samples/client/petstore/typescript-fetch/builds/default/runtime.ts +++ b/samples/client/petstore/typescript-fetch/builds/default/runtime.ts @@ -346,7 +346,7 @@ function querystringSingleKey(key: string, value: string | number | null | undef return querystringSingleKey(key, valueAsArray, keyPrefix); } if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; } if (value instanceof Object) { return querystring(value as HTTPQuery, fullKey); @@ -359,6 +359,58 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + +export function serializeDate(value: Date): string { + if (isNaN(value.getTime())) { + throw new RangeError('Invalid time value'); + } + // Not padStart: the generated client may target ES6, where it does not exist. + const year = ('000' + value.getFullYear()).slice(-4); + const month = ('0' + (value.getMonth() + 1)).slice(-2); + const day = ('0' + value.getDate()).slice(-2); + return `${year}-${month}-${day}`; +} + + +export function parseDate(value: any): Date { + if (value instanceof Date) { + return value; + } + // `new Date("2026-08-05")` would parse as UTC midnight: a different day west of UTC. + // Local midnight is the stated day everywhere. setFullYear avoids the 1900 offset the + // multi-argument constructor applies to years 0-99. + const fullDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value)); + if (fullDate) { + const year = Number(fullDate[1]); + const month = Number(fullDate[2]) - 1; + const day = Number(fullDate[3]); + const date = new Date(0); + date.setFullYear(year, month, day); + date.setHours(0, 0, 0, 0); + // Out-of-range components (or a day the local zone skipped) silently roll over, + // which would hand back a date the server never sent. + if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { + return new Date(NaN); + } + return date; + } + return new Date(value); +} + +export function parseDateTime(value: any): Date { + return new Date(value); +} + export function mapValues(data: any, fn: (item: any) => any) { const result: { [key: string]: any } = {}; for (const key of Object.keys(data)) { diff --git a/samples/client/petstore/typescript-fetch/builds/enum/runtime.ts b/samples/client/petstore/typescript-fetch/builds/enum/runtime.ts index 2d2f267ce4a8..d09ba0070951 100644 --- a/samples/client/petstore/typescript-fetch/builds/enum/runtime.ts +++ b/samples/client/petstore/typescript-fetch/builds/enum/runtime.ts @@ -346,7 +346,7 @@ function querystringSingleKey(key: string, value: string | number | null | undef return querystringSingleKey(key, valueAsArray, keyPrefix); } if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; } if (value instanceof Object) { return querystring(value as HTTPQuery, fullKey); @@ -359,6 +359,58 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + +export function serializeDate(value: Date): string { + if (isNaN(value.getTime())) { + throw new RangeError('Invalid time value'); + } + // Not padStart: the generated client may target ES6, where it does not exist. + const year = ('000' + value.getFullYear()).slice(-4); + const month = ('0' + (value.getMonth() + 1)).slice(-2); + const day = ('0' + value.getDate()).slice(-2); + return `${year}-${month}-${day}`; +} + + +export function parseDate(value: any): Date { + if (value instanceof Date) { + return value; + } + // `new Date("2026-08-05")` would parse as UTC midnight: a different day west of UTC. + // Local midnight is the stated day everywhere. setFullYear avoids the 1900 offset the + // multi-argument constructor applies to years 0-99. + const fullDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value)); + if (fullDate) { + const year = Number(fullDate[1]); + const month = Number(fullDate[2]) - 1; + const day = Number(fullDate[3]); + const date = new Date(0); + date.setFullYear(year, month, day); + date.setHours(0, 0, 0, 0); + // Out-of-range components (or a day the local zone skipped) silently roll over, + // which would hand back a date the server never sent. + if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { + return new Date(NaN); + } + return date; + } + return new Date(value); +} + +export function parseDateTime(value: any): Date { + return new Date(value); +} + export function mapValues(data: any, fn: (item: any) => any) { const result: { [key: string]: any } = {}; for (const key of Object.keys(data)) { diff --git a/samples/client/petstore/typescript-fetch/builds/es6-target/src/models/Order.ts b/samples/client/petstore/typescript-fetch/builds/es6-target/src/models/Order.ts index cb6b5c016de6..e47ffdb65be7 100644 --- a/samples/client/petstore/typescript-fetch/builds/es6-target/src/models/Order.ts +++ b/samples/client/petstore/typescript-fetch/builds/es6-target/src/models/Order.ts @@ -12,7 +12,7 @@ * Do not edit the class manually. */ -import { mapValues } from '../runtime'; +import { mapValues, parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; /** * An order for a pets from the pet store * @export @@ -77,7 +77,7 @@ export function OrderFromJSONTyped(json: any, ignoreDiscriminator: boolean): Ord 'id': json['id'] == null ? undefined : json['id'], 'petId': json['petId'] == null ? undefined : json['petId'], 'quantity': json['quantity'] == null ? undefined : json['quantity'], - 'shipDate': json['shipDate'] == null ? undefined : (new Date(json['shipDate'])), + 'shipDate': json['shipDate'] == null ? undefined : (parseDateTime(json['shipDate'])), 'status': json['status'] == null ? undefined : json['status'], 'complete': json['complete'] == null ? undefined : json['complete'], }; @@ -97,7 +97,7 @@ export function OrderToJSONTyped(value?: Order | null, ignoreDiscriminator: bool 'id': value['id'], 'petId': value['petId'], 'quantity': value['quantity'], - 'shipDate': value['shipDate'] == null ? value['shipDate'] : value['shipDate'].toISOString(), + 'shipDate': value['shipDate'] == null ? value['shipDate'] : serializeDateTime(value['shipDate']), 'status': value['status'], 'complete': value['complete'], }; diff --git a/samples/client/petstore/typescript-fetch/builds/es6-target/src/runtime.ts b/samples/client/petstore/typescript-fetch/builds/es6-target/src/runtime.ts index 3389e4beb1cc..4ea4c5de5589 100644 --- a/samples/client/petstore/typescript-fetch/builds/es6-target/src/runtime.ts +++ b/samples/client/petstore/typescript-fetch/builds/es6-target/src/runtime.ts @@ -346,7 +346,7 @@ function querystringSingleKey(key: string, value: string | number | null | undef return querystringSingleKey(key, valueAsArray, keyPrefix); } if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; } if (value instanceof Object) { return querystring(value as HTTPQuery, fullKey); @@ -359,6 +359,58 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + +export function serializeDate(value: Date): string { + if (isNaN(value.getTime())) { + throw new RangeError('Invalid time value'); + } + // Not padStart: the generated client may target ES6, where it does not exist. + const year = ('000' + value.getFullYear()).slice(-4); + const month = ('0' + (value.getMonth() + 1)).slice(-2); + const day = ('0' + value.getDate()).slice(-2); + return `${year}-${month}-${day}`; +} + + +export function parseDate(value: any): Date { + if (value instanceof Date) { + return value; + } + // `new Date("2026-08-05")` would parse as UTC midnight: a different day west of UTC. + // Local midnight is the stated day everywhere. setFullYear avoids the 1900 offset the + // multi-argument constructor applies to years 0-99. + const fullDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value)); + if (fullDate) { + const year = Number(fullDate[1]); + const month = Number(fullDate[2]) - 1; + const day = Number(fullDate[3]); + const date = new Date(0); + date.setFullYear(year, month, day); + date.setHours(0, 0, 0, 0); + // Out-of-range components (or a day the local zone skipped) silently roll over, + // which would hand back a date the server never sent. + if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { + return new Date(NaN); + } + return date; + } + return new Date(value); +} + +export function parseDateTime(value: any): Date { + return new Date(value); +} + export function mapValues(data: any, fn: (item: any) => any) { const result: { [key: string]: any } = {}; for (const key of Object.keys(data)) { diff --git a/samples/client/petstore/typescript-fetch/builds/kebab-case/apis/fake-api.ts b/samples/client/petstore/typescript-fetch/builds/kebab-case/apis/fake-api.ts index f62eb520f8d1..f5a36c962c75 100644 --- a/samples/client/petstore/typescript-fetch/builds/kebab-case/apis/fake-api.ts +++ b/samples/client/petstore/typescript-fetch/builds/kebab-case/apis/fake-api.ts @@ -1045,11 +1045,11 @@ export class FakeApi extends runtime.BaseAPI { } if (requestParameters['date'] != null) { - formParams.append('date', requestParameters['date'] as any); + formParams.append('date', runtime.serializeDate(requestParameters['date'] as any)); } if (requestParameters['dateTime'] != null) { - formParams.append('dateTime', (requestParameters['dateTime'] as any).toISOString()); + formParams.append('dateTime', runtime.serializeDateTime(requestParameters['dateTime'] as any)); } if (requestParameters['password'] != null) { diff --git a/samples/client/petstore/typescript-fetch/builds/kebab-case/models/format-test.ts b/samples/client/petstore/typescript-fetch/builds/kebab-case/models/format-test.ts index 047bde78c359..aa1f8c7f1518 100644 --- a/samples/client/petstore/typescript-fetch/builds/kebab-case/models/format-test.ts +++ b/samples/client/petstore/typescript-fetch/builds/kebab-case/models/format-test.ts @@ -12,7 +12,7 @@ * Do not edit the class manually. */ -import { mapValues } from '../runtime'; +import { mapValues, parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; /** * * @export @@ -116,8 +116,8 @@ export function FormatTestFromJSONTyped(json: any, ignoreDiscriminator: boolean) 'string': json['string'] == null ? undefined : json['string'], '_byte': json['byte'], 'binary': json['binary'] == null ? undefined : json['binary'], - 'date': (new Date(json['date'])), - 'dateTime': json['dateTime'] == null ? undefined : (new Date(json['dateTime'])), + 'date': (parseDate(json['date'])), + 'dateTime': json['dateTime'] == null ? undefined : (parseDateTime(json['dateTime'])), 'uuid': json['uuid'] == null ? undefined : json['uuid'], 'password': json['password'], 'patternWithDigits': json['pattern_with_digits'] == null ? undefined : json['pattern_with_digits'], @@ -146,8 +146,8 @@ export function FormatTestToJSONTyped(value?: FormatTest | null, ignoreDiscrimin 'string': value['string'], 'byte': value['_byte'], 'binary': value['binary'], - 'date': value['date'].toISOString().substring(0,10), - 'dateTime': value['dateTime'] == null ? value['dateTime'] : value['dateTime'].toISOString(), + 'date': serializeDate(value['date']), + 'dateTime': value['dateTime'] == null ? value['dateTime'] : serializeDateTime(value['dateTime']), 'uuid': value['uuid'], 'password': value['password'], 'pattern_with_digits': value['patternWithDigits'], diff --git a/samples/client/petstore/typescript-fetch/builds/kebab-case/models/mixed-properties-and-additional-properties-class.ts b/samples/client/petstore/typescript-fetch/builds/kebab-case/models/mixed-properties-and-additional-properties-class.ts index 71e0fa8b1ff9..7b5468177b35 100644 --- a/samples/client/petstore/typescript-fetch/builds/kebab-case/models/mixed-properties-and-additional-properties-class.ts +++ b/samples/client/petstore/typescript-fetch/builds/kebab-case/models/mixed-properties-and-additional-properties-class.ts @@ -12,7 +12,7 @@ * Do not edit the class manually. */ -import { mapValues } from '../runtime'; +import { mapValues, parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; import type { Animal } from './animal'; import { AnimalFromJSON, @@ -59,7 +59,7 @@ export function MixedPropertiesAndAdditionalPropertiesClassFromJSONTyped(json: a return { 'uuid': json['uuid'] == null ? undefined : json['uuid'], - 'dateTime': json['dateTime'] == null ? undefined : (new Date(json['dateTime'])), + 'dateTime': json['dateTime'] == null ? undefined : (parseDateTime(json['dateTime'])), 'map': json['map'] == null ? undefined : (mapValues(json['map'], AnimalFromJSON)), }; } @@ -76,7 +76,7 @@ export function MixedPropertiesAndAdditionalPropertiesClassToJSONTyped(value?: M return { 'uuid': value['uuid'], - 'dateTime': value['dateTime'] == null ? value['dateTime'] : value['dateTime'].toISOString(), + 'dateTime': value['dateTime'] == null ? value['dateTime'] : serializeDateTime(value['dateTime']), 'map': value['map'] == null ? undefined : (mapValues(value['map'], AnimalToJSON)), }; } diff --git a/samples/client/petstore/typescript-fetch/builds/kebab-case/models/nullable-class.ts b/samples/client/petstore/typescript-fetch/builds/kebab-case/models/nullable-class.ts index 2899b9f6f3d6..e917dc5109d3 100644 --- a/samples/client/petstore/typescript-fetch/builds/kebab-case/models/nullable-class.ts +++ b/samples/client/petstore/typescript-fetch/builds/kebab-case/models/nullable-class.ts @@ -12,7 +12,7 @@ * Do not edit the class manually. */ -import { mapValues } from '../runtime'; +import { mapValues, parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; /** * * @export @@ -92,8 +92,8 @@ export function NullableClassFromJSONTyped(json: any, ignoreDiscriminator: boole 'numberProp': json['number_prop'] === undefined ? undefined : json['number_prop'] === null ? null : json['number_prop'], 'booleanProp': json['boolean_prop'] === undefined ? undefined : json['boolean_prop'] === null ? null : json['boolean_prop'], 'stringProp': json['string_prop'] === undefined ? undefined : json['string_prop'] === null ? null : json['string_prop'], - 'dateProp': json['date_prop'] === undefined ? undefined : json['date_prop'] === null ? null : (new Date(json['date_prop'])), - 'datetimeProp': json['datetime_prop'] === undefined ? undefined : json['datetime_prop'] === null ? null : (new Date(json['datetime_prop'])), + 'dateProp': json['date_prop'] === undefined ? undefined : json['date_prop'] === null ? null : (parseDate(json['date_prop'])), + 'datetimeProp': json['datetime_prop'] === undefined ? undefined : json['datetime_prop'] === null ? null : (parseDateTime(json['datetime_prop'])), 'arrayNullableProp': json['array_nullable_prop'] === undefined ? undefined : json['array_nullable_prop'] === null ? null : json['array_nullable_prop'], 'arrayAndItemsNullableProp': json['array_and_items_nullable_prop'] === undefined ? undefined : json['array_and_items_nullable_prop'] === null ? null : json['array_and_items_nullable_prop'], 'arrayItemsNullable': json['array_items_nullable'] == null ? undefined : json['array_items_nullable'], @@ -119,8 +119,8 @@ export function NullableClassToJSONTyped(value?: NullableClass | null, ignoreDis 'number_prop': value['numberProp'], 'boolean_prop': value['booleanProp'], 'string_prop': value['stringProp'], - 'date_prop': value['dateProp'] == null ? value['dateProp'] : value['dateProp'].toISOString().substring(0,10), - 'datetime_prop': value['datetimeProp'] == null ? value['datetimeProp'] : value['datetimeProp'].toISOString(), + 'date_prop': value['dateProp'] == null ? value['dateProp'] : serializeDate(value['dateProp']), + 'datetime_prop': value['datetimeProp'] == null ? value['datetimeProp'] : serializeDateTime(value['datetimeProp']), 'array_nullable_prop': value['arrayNullableProp'], 'array_and_items_nullable_prop': value['arrayAndItemsNullableProp'], 'array_items_nullable': value['arrayItemsNullable'], diff --git a/samples/client/petstore/typescript-fetch/builds/kebab-case/models/order.ts b/samples/client/petstore/typescript-fetch/builds/kebab-case/models/order.ts index 1d32da2609f6..8cb7d0bb68bb 100644 --- a/samples/client/petstore/typescript-fetch/builds/kebab-case/models/order.ts +++ b/samples/client/petstore/typescript-fetch/builds/kebab-case/models/order.ts @@ -12,7 +12,7 @@ * Do not edit the class manually. */ -import { mapValues } from '../runtime'; +import { mapValues, parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; /** * * @export @@ -77,7 +77,7 @@ export function OrderFromJSONTyped(json: any, ignoreDiscriminator: boolean): Ord 'id': json['id'] == null ? undefined : json['id'], 'petId': json['petId'] == null ? undefined : json['petId'], 'quantity': json['quantity'] == null ? undefined : json['quantity'], - 'shipDate': json['shipDate'] == null ? undefined : (new Date(json['shipDate'])), + 'shipDate': json['shipDate'] == null ? undefined : (parseDateTime(json['shipDate'])), 'status': json['status'] == null ? undefined : json['status'], 'complete': json['complete'] == null ? undefined : json['complete'], }; @@ -97,7 +97,7 @@ export function OrderToJSONTyped(value?: Order | null, ignoreDiscriminator: bool 'id': value['id'], 'petId': value['petId'], 'quantity': value['quantity'], - 'shipDate': value['shipDate'] == null ? value['shipDate'] : value['shipDate'].toISOString(), + 'shipDate': value['shipDate'] == null ? value['shipDate'] : serializeDateTime(value['shipDate']), 'status': value['status'], 'complete': value['complete'], }; diff --git a/samples/client/petstore/typescript-fetch/builds/kebab-case/runtime.ts b/samples/client/petstore/typescript-fetch/builds/kebab-case/runtime.ts index e52a36947b03..5b823ec12c0d 100644 --- a/samples/client/petstore/typescript-fetch/builds/kebab-case/runtime.ts +++ b/samples/client/petstore/typescript-fetch/builds/kebab-case/runtime.ts @@ -346,7 +346,7 @@ function querystringSingleKey(key: string, value: string | number | null | undef return querystringSingleKey(key, valueAsArray, keyPrefix); } if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; } if (value instanceof Object) { return querystring(value as HTTPQuery, fullKey); @@ -359,6 +359,58 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + +export function serializeDate(value: Date): string { + if (isNaN(value.getTime())) { + throw new RangeError('Invalid time value'); + } + // Not padStart: the generated client may target ES6, where it does not exist. + const year = ('000' + value.getFullYear()).slice(-4); + const month = ('0' + (value.getMonth() + 1)).slice(-2); + const day = ('0' + value.getDate()).slice(-2); + return `${year}-${month}-${day}`; +} + + +export function parseDate(value: any): Date { + if (value instanceof Date) { + return value; + } + // `new Date("2026-08-05")` would parse as UTC midnight: a different day west of UTC. + // Local midnight is the stated day everywhere. setFullYear avoids the 1900 offset the + // multi-argument constructor applies to years 0-99. + const fullDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value)); + if (fullDate) { + const year = Number(fullDate[1]); + const month = Number(fullDate[2]) - 1; + const day = Number(fullDate[3]); + const date = new Date(0); + date.setFullYear(year, month, day); + date.setHours(0, 0, 0, 0); + // Out-of-range components (or a day the local zone skipped) silently roll over, + // which would hand back a date the server never sent. + if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { + return new Date(NaN); + } + return date; + } + return new Date(value); +} + +export function parseDateTime(value: any): Date { + return new Date(value); +} + export function mapValues(data: any, fn: (item: any) => any) { const result: { [key: string]: any } = {}; for (const key of Object.keys(data)) { diff --git a/samples/client/petstore/typescript-fetch/builds/multiple-parameters/models/Order.ts b/samples/client/petstore/typescript-fetch/builds/multiple-parameters/models/Order.ts index cb6b5c016de6..e47ffdb65be7 100644 --- a/samples/client/petstore/typescript-fetch/builds/multiple-parameters/models/Order.ts +++ b/samples/client/petstore/typescript-fetch/builds/multiple-parameters/models/Order.ts @@ -12,7 +12,7 @@ * Do not edit the class manually. */ -import { mapValues } from '../runtime'; +import { mapValues, parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; /** * An order for a pets from the pet store * @export @@ -77,7 +77,7 @@ export function OrderFromJSONTyped(json: any, ignoreDiscriminator: boolean): Ord 'id': json['id'] == null ? undefined : json['id'], 'petId': json['petId'] == null ? undefined : json['petId'], 'quantity': json['quantity'] == null ? undefined : json['quantity'], - 'shipDate': json['shipDate'] == null ? undefined : (new Date(json['shipDate'])), + 'shipDate': json['shipDate'] == null ? undefined : (parseDateTime(json['shipDate'])), 'status': json['status'] == null ? undefined : json['status'], 'complete': json['complete'] == null ? undefined : json['complete'], }; @@ -97,7 +97,7 @@ export function OrderToJSONTyped(value?: Order | null, ignoreDiscriminator: bool 'id': value['id'], 'petId': value['petId'], 'quantity': value['quantity'], - 'shipDate': value['shipDate'] == null ? value['shipDate'] : value['shipDate'].toISOString(), + 'shipDate': value['shipDate'] == null ? value['shipDate'] : serializeDateTime(value['shipDate']), 'status': value['status'], 'complete': value['complete'], }; diff --git a/samples/client/petstore/typescript-fetch/builds/multiple-parameters/runtime.ts b/samples/client/petstore/typescript-fetch/builds/multiple-parameters/runtime.ts index 3389e4beb1cc..4ea4c5de5589 100644 --- a/samples/client/petstore/typescript-fetch/builds/multiple-parameters/runtime.ts +++ b/samples/client/petstore/typescript-fetch/builds/multiple-parameters/runtime.ts @@ -346,7 +346,7 @@ function querystringSingleKey(key: string, value: string | number | null | undef return querystringSingleKey(key, valueAsArray, keyPrefix); } if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; } if (value instanceof Object) { return querystring(value as HTTPQuery, fullKey); @@ -359,6 +359,58 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + +export function serializeDate(value: Date): string { + if (isNaN(value.getTime())) { + throw new RangeError('Invalid time value'); + } + // Not padStart: the generated client may target ES6, where it does not exist. + const year = ('000' + value.getFullYear()).slice(-4); + const month = ('0' + (value.getMonth() + 1)).slice(-2); + const day = ('0' + value.getDate()).slice(-2); + return `${year}-${month}-${day}`; +} + + +export function parseDate(value: any): Date { + if (value instanceof Date) { + return value; + } + // `new Date("2026-08-05")` would parse as UTC midnight: a different day west of UTC. + // Local midnight is the stated day everywhere. setFullYear avoids the 1900 offset the + // multi-argument constructor applies to years 0-99. + const fullDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value)); + if (fullDate) { + const year = Number(fullDate[1]); + const month = Number(fullDate[2]) - 1; + const day = Number(fullDate[3]); + const date = new Date(0); + date.setFullYear(year, month, day); + date.setHours(0, 0, 0, 0); + // Out-of-range components (or a day the local zone skipped) silently roll over, + // which would hand back a date the server never sent. + if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { + return new Date(NaN); + } + return date; + } + return new Date(value); +} + +export function parseDateTime(value: any): Date { + return new Date(value); +} + export function mapValues(data: any, fn: (item: any) => any) { const result: { [key: string]: any } = {}; for (const key of Object.keys(data)) { diff --git a/samples/client/petstore/typescript-fetch/builds/oneOf/models/TestArrayResponse.ts b/samples/client/petstore/typescript-fetch/builds/oneOf/models/TestArrayResponse.ts index e496b66889d1..af3bbd2a6269 100644 --- a/samples/client/petstore/typescript-fetch/builds/oneOf/models/TestArrayResponse.ts +++ b/samples/client/petstore/typescript-fetch/builds/oneOf/models/TestArrayResponse.ts @@ -12,6 +12,7 @@ * Do not edit the class manually. */ +import { parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; import type { TestA } from './TestA'; import { instanceOfTestA, diff --git a/samples/client/petstore/typescript-fetch/builds/oneOf/models/TestDashedDiscriminatorResponse.ts b/samples/client/petstore/typescript-fetch/builds/oneOf/models/TestDashedDiscriminatorResponse.ts index 260fb9107164..6f59236379f1 100644 --- a/samples/client/petstore/typescript-fetch/builds/oneOf/models/TestDashedDiscriminatorResponse.ts +++ b/samples/client/petstore/typescript-fetch/builds/oneOf/models/TestDashedDiscriminatorResponse.ts @@ -12,6 +12,7 @@ * Do not edit the class manually. */ +import { parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; import type { DashedOptionOne } from './DashedOptionOne'; import { instanceOfDashedOptionOne, diff --git a/samples/client/petstore/typescript-fetch/builds/oneOf/models/TestDiscriminatorResponse.ts b/samples/client/petstore/typescript-fetch/builds/oneOf/models/TestDiscriminatorResponse.ts index ae5af3d46ba8..8ebcc75e7186 100644 --- a/samples/client/petstore/typescript-fetch/builds/oneOf/models/TestDiscriminatorResponse.ts +++ b/samples/client/petstore/typescript-fetch/builds/oneOf/models/TestDiscriminatorResponse.ts @@ -12,6 +12,7 @@ * Do not edit the class manually. */ +import { parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; import type { OptionOne } from './OptionOne'; import { instanceOfOptionOne, diff --git a/samples/client/petstore/typescript-fetch/builds/oneOf/models/TestResponse.ts b/samples/client/petstore/typescript-fetch/builds/oneOf/models/TestResponse.ts index 031d545e20bc..ca45996d992e 100644 --- a/samples/client/petstore/typescript-fetch/builds/oneOf/models/TestResponse.ts +++ b/samples/client/petstore/typescript-fetch/builds/oneOf/models/TestResponse.ts @@ -12,6 +12,7 @@ * Do not edit the class manually. */ +import { parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; import type { TestA } from './TestA'; import { instanceOfTestA, diff --git a/samples/client/petstore/typescript-fetch/builds/oneOf/models/TestSnakeCaseDiscriminatorResponse.ts b/samples/client/petstore/typescript-fetch/builds/oneOf/models/TestSnakeCaseDiscriminatorResponse.ts index 34a297e86d47..dfba021f0bbb 100644 --- a/samples/client/petstore/typescript-fetch/builds/oneOf/models/TestSnakeCaseDiscriminatorResponse.ts +++ b/samples/client/petstore/typescript-fetch/builds/oneOf/models/TestSnakeCaseDiscriminatorResponse.ts @@ -12,6 +12,7 @@ * Do not edit the class manually. */ +import { parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; import type { SnakeOptionOne } from './SnakeOptionOne'; import { instanceOfSnakeOptionOne, diff --git a/samples/client/petstore/typescript-fetch/builds/oneOf/runtime.ts b/samples/client/petstore/typescript-fetch/builds/oneOf/runtime.ts index 8c8a505df958..c52d6273d984 100644 --- a/samples/client/petstore/typescript-fetch/builds/oneOf/runtime.ts +++ b/samples/client/petstore/typescript-fetch/builds/oneOf/runtime.ts @@ -346,7 +346,7 @@ function querystringSingleKey(key: string, value: string | number | null | undef return querystringSingleKey(key, valueAsArray, keyPrefix); } if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; } if (value instanceof Object) { return querystring(value as HTTPQuery, fullKey); @@ -359,6 +359,58 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + +export function serializeDate(value: Date): string { + if (isNaN(value.getTime())) { + throw new RangeError('Invalid time value'); + } + // Not padStart: the generated client may target ES6, where it does not exist. + const year = ('000' + value.getFullYear()).slice(-4); + const month = ('0' + (value.getMonth() + 1)).slice(-2); + const day = ('0' + value.getDate()).slice(-2); + return `${year}-${month}-${day}`; +} + + +export function parseDate(value: any): Date { + if (value instanceof Date) { + return value; + } + // `new Date("2026-08-05")` would parse as UTC midnight: a different day west of UTC. + // Local midnight is the stated day everywhere. setFullYear avoids the 1900 offset the + // multi-argument constructor applies to years 0-99. + const fullDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value)); + if (fullDate) { + const year = Number(fullDate[1]); + const month = Number(fullDate[2]) - 1; + const day = Number(fullDate[3]); + const date = new Date(0); + date.setFullYear(year, month, day); + date.setHours(0, 0, 0, 0); + // Out-of-range components (or a day the local zone skipped) silently roll over, + // which would hand back a date the server never sent. + if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { + return new Date(NaN); + } + return date; + } + return new Date(value); +} + +export function parseDateTime(value: any): Date { + return new Date(value); +} + export function mapValues(data: any, fn: (item: any) => any) { const result: { [key: string]: any } = {}; for (const key of Object.keys(data)) { diff --git a/samples/client/petstore/typescript-fetch/builds/prefix-parameter-interfaces/src/models/Order.ts b/samples/client/petstore/typescript-fetch/builds/prefix-parameter-interfaces/src/models/Order.ts index cb6b5c016de6..e47ffdb65be7 100644 --- a/samples/client/petstore/typescript-fetch/builds/prefix-parameter-interfaces/src/models/Order.ts +++ b/samples/client/petstore/typescript-fetch/builds/prefix-parameter-interfaces/src/models/Order.ts @@ -12,7 +12,7 @@ * Do not edit the class manually. */ -import { mapValues } from '../runtime'; +import { mapValues, parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; /** * An order for a pets from the pet store * @export @@ -77,7 +77,7 @@ export function OrderFromJSONTyped(json: any, ignoreDiscriminator: boolean): Ord 'id': json['id'] == null ? undefined : json['id'], 'petId': json['petId'] == null ? undefined : json['petId'], 'quantity': json['quantity'] == null ? undefined : json['quantity'], - 'shipDate': json['shipDate'] == null ? undefined : (new Date(json['shipDate'])), + 'shipDate': json['shipDate'] == null ? undefined : (parseDateTime(json['shipDate'])), 'status': json['status'] == null ? undefined : json['status'], 'complete': json['complete'] == null ? undefined : json['complete'], }; @@ -97,7 +97,7 @@ export function OrderToJSONTyped(value?: Order | null, ignoreDiscriminator: bool 'id': value['id'], 'petId': value['petId'], 'quantity': value['quantity'], - 'shipDate': value['shipDate'] == null ? value['shipDate'] : value['shipDate'].toISOString(), + 'shipDate': value['shipDate'] == null ? value['shipDate'] : serializeDateTime(value['shipDate']), 'status': value['status'], 'complete': value['complete'], }; diff --git a/samples/client/petstore/typescript-fetch/builds/prefix-parameter-interfaces/src/runtime.ts b/samples/client/petstore/typescript-fetch/builds/prefix-parameter-interfaces/src/runtime.ts index 3389e4beb1cc..4ea4c5de5589 100644 --- a/samples/client/petstore/typescript-fetch/builds/prefix-parameter-interfaces/src/runtime.ts +++ b/samples/client/petstore/typescript-fetch/builds/prefix-parameter-interfaces/src/runtime.ts @@ -346,7 +346,7 @@ function querystringSingleKey(key: string, value: string | number | null | undef return querystringSingleKey(key, valueAsArray, keyPrefix); } if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; } if (value instanceof Object) { return querystring(value as HTTPQuery, fullKey); @@ -359,6 +359,58 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + +export function serializeDate(value: Date): string { + if (isNaN(value.getTime())) { + throw new RangeError('Invalid time value'); + } + // Not padStart: the generated client may target ES6, where it does not exist. + const year = ('000' + value.getFullYear()).slice(-4); + const month = ('0' + (value.getMonth() + 1)).slice(-2); + const day = ('0' + value.getDate()).slice(-2); + return `${year}-${month}-${day}`; +} + + +export function parseDate(value: any): Date { + if (value instanceof Date) { + return value; + } + // `new Date("2026-08-05")` would parse as UTC midnight: a different day west of UTC. + // Local midnight is the stated day everywhere. setFullYear avoids the 1900 offset the + // multi-argument constructor applies to years 0-99. + const fullDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value)); + if (fullDate) { + const year = Number(fullDate[1]); + const month = Number(fullDate[2]) - 1; + const day = Number(fullDate[3]); + const date = new Date(0); + date.setFullYear(year, month, day); + date.setHours(0, 0, 0, 0); + // Out-of-range components (or a day the local zone skipped) silently roll over, + // which would hand back a date the server never sent. + if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { + return new Date(NaN); + } + return date; + } + return new Date(value); +} + +export function parseDateTime(value: any): Date { + return new Date(value); +} + export function mapValues(data: any, fn: (item: any) => any) { const result: { [key: string]: any } = {}; for (const key of Object.keys(data)) { diff --git a/samples/client/petstore/typescript-fetch/builds/sagas-and-records/src/models/Order.ts b/samples/client/petstore/typescript-fetch/builds/sagas-and-records/src/models/Order.ts index cb6b5c016de6..e47ffdb65be7 100644 --- a/samples/client/petstore/typescript-fetch/builds/sagas-and-records/src/models/Order.ts +++ b/samples/client/petstore/typescript-fetch/builds/sagas-and-records/src/models/Order.ts @@ -12,7 +12,7 @@ * Do not edit the class manually. */ -import { mapValues } from '../runtime'; +import { mapValues, parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; /** * An order for a pets from the pet store * @export @@ -77,7 +77,7 @@ export function OrderFromJSONTyped(json: any, ignoreDiscriminator: boolean): Ord 'id': json['id'] == null ? undefined : json['id'], 'petId': json['petId'] == null ? undefined : json['petId'], 'quantity': json['quantity'] == null ? undefined : json['quantity'], - 'shipDate': json['shipDate'] == null ? undefined : (new Date(json['shipDate'])), + 'shipDate': json['shipDate'] == null ? undefined : (parseDateTime(json['shipDate'])), 'status': json['status'] == null ? undefined : json['status'], 'complete': json['complete'] == null ? undefined : json['complete'], }; @@ -97,7 +97,7 @@ export function OrderToJSONTyped(value?: Order | null, ignoreDiscriminator: bool 'id': value['id'], 'petId': value['petId'], 'quantity': value['quantity'], - 'shipDate': value['shipDate'] == null ? value['shipDate'] : value['shipDate'].toISOString(), + 'shipDate': value['shipDate'] == null ? value['shipDate'] : serializeDateTime(value['shipDate']), 'status': value['status'], 'complete': value['complete'], }; diff --git a/samples/client/petstore/typescript-fetch/builds/sagas-and-records/src/runtime.ts b/samples/client/petstore/typescript-fetch/builds/sagas-and-records/src/runtime.ts index 3389e4beb1cc..4ea4c5de5589 100644 --- a/samples/client/petstore/typescript-fetch/builds/sagas-and-records/src/runtime.ts +++ b/samples/client/petstore/typescript-fetch/builds/sagas-and-records/src/runtime.ts @@ -346,7 +346,7 @@ function querystringSingleKey(key: string, value: string | number | null | undef return querystringSingleKey(key, valueAsArray, keyPrefix); } if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; } if (value instanceof Object) { return querystring(value as HTTPQuery, fullKey); @@ -359,6 +359,58 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + +export function serializeDate(value: Date): string { + if (isNaN(value.getTime())) { + throw new RangeError('Invalid time value'); + } + // Not padStart: the generated client may target ES6, where it does not exist. + const year = ('000' + value.getFullYear()).slice(-4); + const month = ('0' + (value.getMonth() + 1)).slice(-2); + const day = ('0' + value.getDate()).slice(-2); + return `${year}-${month}-${day}`; +} + + +export function parseDate(value: any): Date { + if (value instanceof Date) { + return value; + } + // `new Date("2026-08-05")` would parse as UTC midnight: a different day west of UTC. + // Local midnight is the stated day everywhere. setFullYear avoids the 1900 offset the + // multi-argument constructor applies to years 0-99. + const fullDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value)); + if (fullDate) { + const year = Number(fullDate[1]); + const month = Number(fullDate[2]) - 1; + const day = Number(fullDate[3]); + const date = new Date(0); + date.setFullYear(year, month, day); + date.setHours(0, 0, 0, 0); + // Out-of-range components (or a day the local zone skipped) silently roll over, + // which would hand back a date the server never sent. + if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { + return new Date(NaN); + } + return date; + } + return new Date(value); +} + +export function parseDateTime(value: any): Date { + return new Date(value); +} + export function mapValues(data: any, fn: (item: any) => any) { const result: { [key: string]: any } = {}; for (const key of Object.keys(data)) { diff --git a/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/apis/FakeApi.ts b/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/apis/FakeApi.ts index 9514b1d2ab5a..168fee1b1454 100644 --- a/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/apis/FakeApi.ts +++ b/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/apis/FakeApi.ts @@ -959,11 +959,11 @@ export class FakeApi extends runtime.BaseAPI { } if (requestParameters['date'] != null) { - formParams.append('date', requestParameters['date'] as any); + formParams.append('date', runtime.serializeDate(requestParameters['date'] as any)); } if (requestParameters['dateTime'] != null) { - formParams.append('dateTime', (requestParameters['dateTime'] as any).toISOString()); + formParams.append('dateTime', runtime.serializeDateTime(requestParameters['dateTime'] as any)); } if (requestParameters['password'] != null) { diff --git a/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/models/FormatTest.ts b/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/models/FormatTest.ts index 047bde78c359..aa1f8c7f1518 100644 --- a/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/models/FormatTest.ts +++ b/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/models/FormatTest.ts @@ -12,7 +12,7 @@ * Do not edit the class manually. */ -import { mapValues } from '../runtime'; +import { mapValues, parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; /** * * @export @@ -116,8 +116,8 @@ export function FormatTestFromJSONTyped(json: any, ignoreDiscriminator: boolean) 'string': json['string'] == null ? undefined : json['string'], '_byte': json['byte'], 'binary': json['binary'] == null ? undefined : json['binary'], - 'date': (new Date(json['date'])), - 'dateTime': json['dateTime'] == null ? undefined : (new Date(json['dateTime'])), + 'date': (parseDate(json['date'])), + 'dateTime': json['dateTime'] == null ? undefined : (parseDateTime(json['dateTime'])), 'uuid': json['uuid'] == null ? undefined : json['uuid'], 'password': json['password'], 'patternWithDigits': json['pattern_with_digits'] == null ? undefined : json['pattern_with_digits'], @@ -146,8 +146,8 @@ export function FormatTestToJSONTyped(value?: FormatTest | null, ignoreDiscrimin 'string': value['string'], 'byte': value['_byte'], 'binary': value['binary'], - 'date': value['date'].toISOString().substring(0,10), - 'dateTime': value['dateTime'] == null ? value['dateTime'] : value['dateTime'].toISOString(), + 'date': serializeDate(value['date']), + 'dateTime': value['dateTime'] == null ? value['dateTime'] : serializeDateTime(value['dateTime']), 'uuid': value['uuid'], 'password': value['password'], 'pattern_with_digits': value['patternWithDigits'], diff --git a/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/models/MixedPropertiesAndAdditionalPropertiesClass.ts b/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/models/MixedPropertiesAndAdditionalPropertiesClass.ts index cb7a2989f0c9..29e136055639 100644 --- a/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/models/MixedPropertiesAndAdditionalPropertiesClass.ts +++ b/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/models/MixedPropertiesAndAdditionalPropertiesClass.ts @@ -12,7 +12,7 @@ * Do not edit the class manually. */ -import { mapValues } from '../runtime'; +import { mapValues, parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; import type { Animal } from './Animal'; import { AnimalFromJSON, @@ -59,7 +59,7 @@ export function MixedPropertiesAndAdditionalPropertiesClassFromJSONTyped(json: a return { 'uuid': json['uuid'] == null ? undefined : json['uuid'], - 'dateTime': json['dateTime'] == null ? undefined : (new Date(json['dateTime'])), + 'dateTime': json['dateTime'] == null ? undefined : (parseDateTime(json['dateTime'])), 'map': json['map'] == null ? undefined : (mapValues(json['map'], AnimalFromJSON)), }; } @@ -76,7 +76,7 @@ export function MixedPropertiesAndAdditionalPropertiesClassToJSONTyped(value?: M return { 'uuid': value['uuid'], - 'dateTime': value['dateTime'] == null ? value['dateTime'] : value['dateTime'].toISOString(), + 'dateTime': value['dateTime'] == null ? value['dateTime'] : serializeDateTime(value['dateTime']), 'map': value['map'] == null ? undefined : (mapValues(value['map'], AnimalToJSON)), }; } diff --git a/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/models/NullableClass.ts b/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/models/NullableClass.ts index 2899b9f6f3d6..e917dc5109d3 100644 --- a/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/models/NullableClass.ts +++ b/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/models/NullableClass.ts @@ -12,7 +12,7 @@ * Do not edit the class manually. */ -import { mapValues } from '../runtime'; +import { mapValues, parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; /** * * @export @@ -92,8 +92,8 @@ export function NullableClassFromJSONTyped(json: any, ignoreDiscriminator: boole 'numberProp': json['number_prop'] === undefined ? undefined : json['number_prop'] === null ? null : json['number_prop'], 'booleanProp': json['boolean_prop'] === undefined ? undefined : json['boolean_prop'] === null ? null : json['boolean_prop'], 'stringProp': json['string_prop'] === undefined ? undefined : json['string_prop'] === null ? null : json['string_prop'], - 'dateProp': json['date_prop'] === undefined ? undefined : json['date_prop'] === null ? null : (new Date(json['date_prop'])), - 'datetimeProp': json['datetime_prop'] === undefined ? undefined : json['datetime_prop'] === null ? null : (new Date(json['datetime_prop'])), + 'dateProp': json['date_prop'] === undefined ? undefined : json['date_prop'] === null ? null : (parseDate(json['date_prop'])), + 'datetimeProp': json['datetime_prop'] === undefined ? undefined : json['datetime_prop'] === null ? null : (parseDateTime(json['datetime_prop'])), 'arrayNullableProp': json['array_nullable_prop'] === undefined ? undefined : json['array_nullable_prop'] === null ? null : json['array_nullable_prop'], 'arrayAndItemsNullableProp': json['array_and_items_nullable_prop'] === undefined ? undefined : json['array_and_items_nullable_prop'] === null ? null : json['array_and_items_nullable_prop'], 'arrayItemsNullable': json['array_items_nullable'] == null ? undefined : json['array_items_nullable'], @@ -119,8 +119,8 @@ export function NullableClassToJSONTyped(value?: NullableClass | null, ignoreDis 'number_prop': value['numberProp'], 'boolean_prop': value['booleanProp'], 'string_prop': value['stringProp'], - 'date_prop': value['dateProp'] == null ? value['dateProp'] : value['dateProp'].toISOString().substring(0,10), - 'datetime_prop': value['datetimeProp'] == null ? value['datetimeProp'] : value['datetimeProp'].toISOString(), + 'date_prop': value['dateProp'] == null ? value['dateProp'] : serializeDate(value['dateProp']), + 'datetime_prop': value['datetimeProp'] == null ? value['datetimeProp'] : serializeDateTime(value['datetimeProp']), 'array_nullable_prop': value['arrayNullableProp'], 'array_and_items_nullable_prop': value['arrayAndItemsNullableProp'], 'array_items_nullable': value['arrayItemsNullable'], diff --git a/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/models/Order.ts b/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/models/Order.ts index 1d32da2609f6..8cb7d0bb68bb 100644 --- a/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/models/Order.ts +++ b/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/models/Order.ts @@ -12,7 +12,7 @@ * Do not edit the class manually. */ -import { mapValues } from '../runtime'; +import { mapValues, parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; /** * * @export @@ -77,7 +77,7 @@ export function OrderFromJSONTyped(json: any, ignoreDiscriminator: boolean): Ord 'id': json['id'] == null ? undefined : json['id'], 'petId': json['petId'] == null ? undefined : json['petId'], 'quantity': json['quantity'] == null ? undefined : json['quantity'], - 'shipDate': json['shipDate'] == null ? undefined : (new Date(json['shipDate'])), + 'shipDate': json['shipDate'] == null ? undefined : (parseDateTime(json['shipDate'])), 'status': json['status'] == null ? undefined : json['status'], 'complete': json['complete'] == null ? undefined : json['complete'], }; @@ -97,7 +97,7 @@ export function OrderToJSONTyped(value?: Order | null, ignoreDiscriminator: bool 'id': value['id'], 'petId': value['petId'], 'quantity': value['quantity'], - 'shipDate': value['shipDate'] == null ? value['shipDate'] : value['shipDate'].toISOString(), + 'shipDate': value['shipDate'] == null ? value['shipDate'] : serializeDateTime(value['shipDate']), 'status': value['status'], 'complete': value['complete'], }; diff --git a/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/runtime.ts b/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/runtime.ts index e52a36947b03..5b823ec12c0d 100644 --- a/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/runtime.ts +++ b/samples/client/petstore/typescript-fetch/builds/snakecase-discriminator/runtime.ts @@ -346,7 +346,7 @@ function querystringSingleKey(key: string, value: string | number | null | undef return querystringSingleKey(key, valueAsArray, keyPrefix); } if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; } if (value instanceof Object) { return querystring(value as HTTPQuery, fullKey); @@ -359,6 +359,58 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + +export function serializeDate(value: Date): string { + if (isNaN(value.getTime())) { + throw new RangeError('Invalid time value'); + } + // Not padStart: the generated client may target ES6, where it does not exist. + const year = ('000' + value.getFullYear()).slice(-4); + const month = ('0' + (value.getMonth() + 1)).slice(-2); + const day = ('0' + value.getDate()).slice(-2); + return `${year}-${month}-${day}`; +} + + +export function parseDate(value: any): Date { + if (value instanceof Date) { + return value; + } + // `new Date("2026-08-05")` would parse as UTC midnight: a different day west of UTC. + // Local midnight is the stated day everywhere. setFullYear avoids the 1900 offset the + // multi-argument constructor applies to years 0-99. + const fullDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value)); + if (fullDate) { + const year = Number(fullDate[1]); + const month = Number(fullDate[2]) - 1; + const day = Number(fullDate[3]); + const date = new Date(0); + date.setFullYear(year, month, day); + date.setHours(0, 0, 0, 0); + // Out-of-range components (or a day the local zone skipped) silently roll over, + // which would hand back a date the server never sent. + if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { + return new Date(NaN); + } + return date; + } + return new Date(value); +} + +export function parseDateTime(value: any): Date { + return new Date(value); +} + export function mapValues(data: any, fn: (item: any) => any) { const result: { [key: string]: any } = {}; for (const key of Object.keys(data)) { diff --git a/samples/client/petstore/typescript-fetch/builds/validation-attributes/models/Order.ts b/samples/client/petstore/typescript-fetch/builds/validation-attributes/models/Order.ts index 0ad9b3725c98..cbe0a8b71b0e 100644 --- a/samples/client/petstore/typescript-fetch/builds/validation-attributes/models/Order.ts +++ b/samples/client/petstore/typescript-fetch/builds/validation-attributes/models/Order.ts @@ -12,7 +12,7 @@ * Do not edit the class manually. */ -import { mapValues } from '../runtime'; +import { mapValues, parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; /** * An order for a pets from the pet store * @export @@ -103,7 +103,7 @@ export function OrderFromJSONTyped(json: any, ignoreDiscriminator: boolean): Ord 'id': json['id'] == null ? undefined : json['id'], 'petId': json['petId'] == null ? undefined : json['petId'], 'quantity': json['quantity'] == null ? undefined : json['quantity'], - 'shipDate': json['shipDate'] == null ? undefined : (new Date(json['shipDate'])), + 'shipDate': json['shipDate'] == null ? undefined : (parseDateTime(json['shipDate'])), 'status': json['status'] == null ? undefined : json['status'], 'complete': json['complete'] == null ? undefined : json['complete'], }; @@ -124,7 +124,7 @@ export function OrderToJSONTyped(value?: Order | null, ignoreDiscriminator: bool 'id': value['id'], 'petId': value['petId'], 'quantity': value['quantity'], - 'shipDate': value['shipDate'] == null ? value['shipDate'] : value['shipDate'].toISOString(), + 'shipDate': value['shipDate'] == null ? value['shipDate'] : serializeDateTime(value['shipDate']), 'status': value['status'], 'complete': value['complete'], }; diff --git a/samples/client/petstore/typescript-fetch/builds/validation-attributes/runtime.ts b/samples/client/petstore/typescript-fetch/builds/validation-attributes/runtime.ts index 3389e4beb1cc..4ea4c5de5589 100644 --- a/samples/client/petstore/typescript-fetch/builds/validation-attributes/runtime.ts +++ b/samples/client/petstore/typescript-fetch/builds/validation-attributes/runtime.ts @@ -346,7 +346,7 @@ function querystringSingleKey(key: string, value: string | number | null | undef return querystringSingleKey(key, valueAsArray, keyPrefix); } if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; } if (value instanceof Object) { return querystring(value as HTTPQuery, fullKey); @@ -359,6 +359,58 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + +export function serializeDate(value: Date): string { + if (isNaN(value.getTime())) { + throw new RangeError('Invalid time value'); + } + // Not padStart: the generated client may target ES6, where it does not exist. + const year = ('000' + value.getFullYear()).slice(-4); + const month = ('0' + (value.getMonth() + 1)).slice(-2); + const day = ('0' + value.getDate()).slice(-2); + return `${year}-${month}-${day}`; +} + + +export function parseDate(value: any): Date { + if (value instanceof Date) { + return value; + } + // `new Date("2026-08-05")` would parse as UTC midnight: a different day west of UTC. + // Local midnight is the stated day everywhere. setFullYear avoids the 1900 offset the + // multi-argument constructor applies to years 0-99. + const fullDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value)); + if (fullDate) { + const year = Number(fullDate[1]); + const month = Number(fullDate[2]) - 1; + const day = Number(fullDate[3]); + const date = new Date(0); + date.setFullYear(year, month, day); + date.setHours(0, 0, 0, 0); + // Out-of-range components (or a day the local zone skipped) silently roll over, + // which would hand back a date the server never sent. + if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { + return new Date(NaN); + } + return date; + } + return new Date(value); +} + +export function parseDateTime(value: any): Date { + return new Date(value); +} + export function mapValues(data: any, fn: (item: any) => any) { const result: { [key: string]: any } = {}; for (const key of Object.keys(data)) { diff --git a/samples/client/petstore/typescript-fetch/builds/with-interfaces/models/Order.ts b/samples/client/petstore/typescript-fetch/builds/with-interfaces/models/Order.ts index cb6b5c016de6..e47ffdb65be7 100644 --- a/samples/client/petstore/typescript-fetch/builds/with-interfaces/models/Order.ts +++ b/samples/client/petstore/typescript-fetch/builds/with-interfaces/models/Order.ts @@ -12,7 +12,7 @@ * Do not edit the class manually. */ -import { mapValues } from '../runtime'; +import { mapValues, parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; /** * An order for a pets from the pet store * @export @@ -77,7 +77,7 @@ export function OrderFromJSONTyped(json: any, ignoreDiscriminator: boolean): Ord 'id': json['id'] == null ? undefined : json['id'], 'petId': json['petId'] == null ? undefined : json['petId'], 'quantity': json['quantity'] == null ? undefined : json['quantity'], - 'shipDate': json['shipDate'] == null ? undefined : (new Date(json['shipDate'])), + 'shipDate': json['shipDate'] == null ? undefined : (parseDateTime(json['shipDate'])), 'status': json['status'] == null ? undefined : json['status'], 'complete': json['complete'] == null ? undefined : json['complete'], }; @@ -97,7 +97,7 @@ export function OrderToJSONTyped(value?: Order | null, ignoreDiscriminator: bool 'id': value['id'], 'petId': value['petId'], 'quantity': value['quantity'], - 'shipDate': value['shipDate'] == null ? value['shipDate'] : value['shipDate'].toISOString(), + 'shipDate': value['shipDate'] == null ? value['shipDate'] : serializeDateTime(value['shipDate']), 'status': value['status'], 'complete': value['complete'], }; diff --git a/samples/client/petstore/typescript-fetch/builds/with-interfaces/runtime.ts b/samples/client/petstore/typescript-fetch/builds/with-interfaces/runtime.ts index 3389e4beb1cc..4ea4c5de5589 100644 --- a/samples/client/petstore/typescript-fetch/builds/with-interfaces/runtime.ts +++ b/samples/client/petstore/typescript-fetch/builds/with-interfaces/runtime.ts @@ -346,7 +346,7 @@ function querystringSingleKey(key: string, value: string | number | null | undef return querystringSingleKey(key, valueAsArray, keyPrefix); } if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; } if (value instanceof Object) { return querystring(value as HTTPQuery, fullKey); @@ -359,6 +359,58 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + +export function serializeDate(value: Date): string { + if (isNaN(value.getTime())) { + throw new RangeError('Invalid time value'); + } + // Not padStart: the generated client may target ES6, where it does not exist. + const year = ('000' + value.getFullYear()).slice(-4); + const month = ('0' + (value.getMonth() + 1)).slice(-2); + const day = ('0' + value.getDate()).slice(-2); + return `${year}-${month}-${day}`; +} + + +export function parseDate(value: any): Date { + if (value instanceof Date) { + return value; + } + // `new Date("2026-08-05")` would parse as UTC midnight: a different day west of UTC. + // Local midnight is the stated day everywhere. setFullYear avoids the 1900 offset the + // multi-argument constructor applies to years 0-99. + const fullDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value)); + if (fullDate) { + const year = Number(fullDate[1]); + const month = Number(fullDate[2]) - 1; + const day = Number(fullDate[3]); + const date = new Date(0); + date.setFullYear(year, month, day); + date.setHours(0, 0, 0, 0); + // Out-of-range components (or a day the local zone skipped) silently roll over, + // which would hand back a date the server never sent. + if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { + return new Date(NaN); + } + return date; + } + return new Date(value); +} + +export function parseDateTime(value: any): Date { + return new Date(value); +} + export function mapValues(data: any, fn: (item: any) => any) { const result: { [key: string]: any } = {}; for (const key of Object.keys(data)) { diff --git a/samples/client/petstore/typescript-fetch/builds/with-npm-version/src/models/Order.ts b/samples/client/petstore/typescript-fetch/builds/with-npm-version/src/models/Order.ts index cb6b5c016de6..e47ffdb65be7 100644 --- a/samples/client/petstore/typescript-fetch/builds/with-npm-version/src/models/Order.ts +++ b/samples/client/petstore/typescript-fetch/builds/with-npm-version/src/models/Order.ts @@ -12,7 +12,7 @@ * Do not edit the class manually. */ -import { mapValues } from '../runtime'; +import { mapValues, parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime'; /** * An order for a pets from the pet store * @export @@ -77,7 +77,7 @@ export function OrderFromJSONTyped(json: any, ignoreDiscriminator: boolean): Ord 'id': json['id'] == null ? undefined : json['id'], 'petId': json['petId'] == null ? undefined : json['petId'], 'quantity': json['quantity'] == null ? undefined : json['quantity'], - 'shipDate': json['shipDate'] == null ? undefined : (new Date(json['shipDate'])), + 'shipDate': json['shipDate'] == null ? undefined : (parseDateTime(json['shipDate'])), 'status': json['status'] == null ? undefined : json['status'], 'complete': json['complete'] == null ? undefined : json['complete'], }; @@ -97,7 +97,7 @@ export function OrderToJSONTyped(value?: Order | null, ignoreDiscriminator: bool 'id': value['id'], 'petId': value['petId'], 'quantity': value['quantity'], - 'shipDate': value['shipDate'] == null ? value['shipDate'] : value['shipDate'].toISOString(), + 'shipDate': value['shipDate'] == null ? value['shipDate'] : serializeDateTime(value['shipDate']), 'status': value['status'], 'complete': value['complete'], }; diff --git a/samples/client/petstore/typescript-fetch/builds/with-npm-version/src/runtime.ts b/samples/client/petstore/typescript-fetch/builds/with-npm-version/src/runtime.ts index 3389e4beb1cc..4ea4c5de5589 100644 --- a/samples/client/petstore/typescript-fetch/builds/with-npm-version/src/runtime.ts +++ b/samples/client/petstore/typescript-fetch/builds/with-npm-version/src/runtime.ts @@ -346,7 +346,7 @@ function querystringSingleKey(key: string, value: string | number | null | undef return querystringSingleKey(key, valueAsArray, keyPrefix); } if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; } if (value instanceof Object) { return querystring(value as HTTPQuery, fullKey); @@ -359,6 +359,58 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + +export function serializeDate(value: Date): string { + if (isNaN(value.getTime())) { + throw new RangeError('Invalid time value'); + } + // Not padStart: the generated client may target ES6, where it does not exist. + const year = ('000' + value.getFullYear()).slice(-4); + const month = ('0' + (value.getMonth() + 1)).slice(-2); + const day = ('0' + value.getDate()).slice(-2); + return `${year}-${month}-${day}`; +} + + +export function parseDate(value: any): Date { + if (value instanceof Date) { + return value; + } + // `new Date("2026-08-05")` would parse as UTC midnight: a different day west of UTC. + // Local midnight is the stated day everywhere. setFullYear avoids the 1900 offset the + // multi-argument constructor applies to years 0-99. + const fullDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value)); + if (fullDate) { + const year = Number(fullDate[1]); + const month = Number(fullDate[2]) - 1; + const day = Number(fullDate[3]); + const date = new Date(0); + date.setFullYear(year, month, day); + date.setHours(0, 0, 0, 0); + // Out-of-range components (or a day the local zone skipped) silently roll over, + // which would hand back a date the server never sent. + if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { + return new Date(NaN); + } + return date; + } + return new Date(value); +} + +export function parseDateTime(value: any): Date { + return new Date(value); +} + export function mapValues(data: any, fn: (item: any) => any) { const result: { [key: string]: any } = {}; for (const key of Object.keys(data)) { diff --git a/samples/client/petstore/typescript-fetch/builds/with-string-enums/runtime.ts b/samples/client/petstore/typescript-fetch/builds/with-string-enums/runtime.ts index 2d2f267ce4a8..d09ba0070951 100644 --- a/samples/client/petstore/typescript-fetch/builds/with-string-enums/runtime.ts +++ b/samples/client/petstore/typescript-fetch/builds/with-string-enums/runtime.ts @@ -346,7 +346,7 @@ function querystringSingleKey(key: string, value: string | number | null | undef return querystringSingleKey(key, valueAsArray, keyPrefix); } if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; } if (value instanceof Object) { return querystring(value as HTTPQuery, fullKey); @@ -359,6 +359,58 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + +export function serializeDate(value: Date): string { + if (isNaN(value.getTime())) { + throw new RangeError('Invalid time value'); + } + // Not padStart: the generated client may target ES6, where it does not exist. + const year = ('000' + value.getFullYear()).slice(-4); + const month = ('0' + (value.getMonth() + 1)).slice(-2); + const day = ('0' + value.getDate()).slice(-2); + return `${year}-${month}-${day}`; +} + + +export function parseDate(value: any): Date { + if (value instanceof Date) { + return value; + } + // `new Date("2026-08-05")` would parse as UTC midnight: a different day west of UTC. + // Local midnight is the stated day everywhere. setFullYear avoids the 1900 offset the + // multi-argument constructor applies to years 0-99. + const fullDate = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value)); + if (fullDate) { + const year = Number(fullDate[1]); + const month = Number(fullDate[2]) - 1; + const day = Number(fullDate[3]); + const date = new Date(0); + date.setFullYear(year, month, day); + date.setHours(0, 0, 0, 0); + // Out-of-range components (or a day the local zone skipped) silently roll over, + // which would hand back a date the server never sent. + if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) { + return new Date(NaN); + } + return date; + } + return new Date(value); +} + +export function parseDateTime(value: any): Date { + return new Date(value); +} + export function mapValues(data: any, fn: (item: any) => any) { const result: { [key: string]: any } = {}; for (const key of Object.keys(data)) { diff --git a/samples/client/petstore/typescript-fetch/builds/without-runtime-checks/src/runtime.ts b/samples/client/petstore/typescript-fetch/builds/without-runtime-checks/src/runtime.ts index 45dfc650f077..56e2e516c01a 100644 --- a/samples/client/petstore/typescript-fetch/builds/without-runtime-checks/src/runtime.ts +++ b/samples/client/petstore/typescript-fetch/builds/without-runtime-checks/src/runtime.ts @@ -346,7 +346,7 @@ function querystringSingleKey(key: string, value: string | number | null | undef return querystringSingleKey(key, valueAsArray, keyPrefix); } if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; + return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`; } if (value instanceof Object) { return querystring(value as HTTPQuery, fullKey); @@ -359,6 +359,18 @@ export function exists(json: any, key: string) { return value !== null && value !== undefined; } +/** + * Every generated date call site routes through these. + * + * `format: date` is a calendar date, with no time and no offset, so it is converted + * against the local calendar on both ends: they have to agree or the date shifts by + * a day. `format: date-time` is an instant and uses UTC. + */ +export function serializeDateTime(value: Date): string { + return value.toISOString(); +} + + export function canConsumeForm(consumes: Consume[]): boolean { for (const consume of consumes) {