Skip to content

Commit 88aeaad

Browse files
authored
[typescript-fetch] Add Temporal support (#24924)
* [typescript-fetch] Add Temporal support This adds a new date library option to the typescript-fetch generator: "temporal". With it, the generator uses Temporal.Instant for "type: string, format: date-time" and Temporal.PlainDate for "type: string, format: date". * Improve solution Based on comments from cubic-dev-ai.
1 parent ff9b3f1 commit 88aeaad

7 files changed

Lines changed: 184 additions & 8 deletions

File tree

‎docs/generators/typescript-fetch.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
1919
| Option | Description | Values | Default |
2020
| ------ | ----------- | ------ | ------- |
2121
|allowUnicodeIdentifiers|boolean, toggles whether unicode identifiers are allowed in names or not, default is false| |false|
22-
|dateLibrary|Option. Date library to use.|<dl><dt>**date**</dt><dd>Native Date. `format: date` and `format: date-time` are both mapped to Date and (de)serialized by the runtime.</dd><dt>**string**</dt><dd>Plain string. Values are passed through untouched, leaving date handling to the consumer.</dd></dl>|date|
22+
|dateLibrary|Option. Date library to use.|<dl><dt>**date**</dt><dd>Native Date. `format: date` and `format: date-time` are both mapped to Date and (de)serialized by the runtime.</dd><dt>**string**</dt><dd>Plain string. Values are passed through untouched, leaving date handling to the consumer.</dd><dt>**temporal**</dt><dd>Native Temporal. `format: date` is mapped to Temporal.PlainDate and `format: date-time` is mapped to Temporal.Instant and (de)serialized by the runtime.</dd></dl>|date|
2323
|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.|<dl><dt>**false**</dt><dd>The 'additionalProperties' implementation is compliant with the OAS and JSON schema specifications.</dd><dt>**true**</dt><dd>Keep the old (incorrect) behaviour that 'additionalProperties' is set to false by default.</dd></dl>|true|
2424
|ensureUniqueParams|Whether to ensure parameter names are unique in an operation (rename parameters that are not).| |true|
2525
|enumNameSuffix|Suffix that will be appended to all enum names.| |Enum|
@@ -99,6 +99,8 @@ These options may be applied as additional-properties (cli) or configOptions (pl
9999
<li>ReturnType</li>
100100
<li>Set</li>
101101
<li>String</li>
102+
<li>Temporal.Instant</li>
103+
<li>Temporal.PlainDate</li>
102104
<li>ThisParameterType</li>
103105
<li>ThisType</li>
104106
<li>Uncapitalize</li>

‎modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/TypeScriptFetchClientCodegen.java‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public class TypeScriptFetchClientCodegen extends AbstractTypeScriptClientCodege
6767
public static final String DATE_LIBRARY_DESC = "Option. Date library to use.";
6868
public static final String DATE_LIBRARY_DATE = "date";
6969
public static final String DATE_LIBRARY_STRING = "string";
70+
public static final String DATE_LIBRARY_TEMPORAL = "temporal";
7071
public static final String STRING_ENUMS = "stringEnums";
7172
public static final String STRING_ENUMS_DESC = "Generate string enums instead of objects for enum values.";
7273
public static final String IMPORT_FILE_EXTENSION_SWITCH = "importFileExtension";
@@ -112,6 +113,8 @@ public class TypeScriptFetchClientCodegen extends AbstractTypeScriptClientCodege
112113
private static final String DATE_TYPE = "date";
113114
private static final String DATE_TIME_TYPE = "DateTime";
114115
private static final String TS_DATE_TYPE = "Date";
116+
private static final String TS_TEMPORAL_INSTANT_TYPE = "Temporal.Instant";
117+
private static final String TS_TEMPORAL_PLAIN_DATE_TYPE = "Temporal.PlainDate";
115118

116119
protected boolean sagasAndRecords = false;
117120
@Getter @Setter
@@ -143,6 +146,11 @@ public TypeScriptFetchClientCodegen() {
143146

144147
this.addExtraReservedWords();
145148

149+
languageSpecificPrimitives.addAll(Arrays.asList(
150+
TS_TEMPORAL_INSTANT_TYPE,
151+
TS_TEMPORAL_PLAIN_DATE_TYPE
152+
));
153+
146154
supportModelPropertyNaming(CodegenConstants.MODEL_PROPERTY_NAMING_TYPE.camelCase);
147155
this.cliOptions.add(new CliOption(NPM_REPOSITORY, "Use this property to set an url your private npmRepo in the package.json"));
148156
this.cliOptions.add(new CliOption(WITH_INTERFACES, "Setting this property to true will generate interfaces next to the default class implementations.", SchemaTypeUtil.BOOLEAN_TYPE).defaultValue(Boolean.FALSE.toString()));
@@ -154,6 +162,7 @@ public TypeScriptFetchClientCodegen() {
154162
Map<String, String> dateOptions = new HashMap<>();
155163
dateOptions.put(DATE_LIBRARY_DATE, "Native Date. `format: date` and `format: date-time` are both mapped to Date and (de)serialized by the runtime.");
156164
dateOptions.put(DATE_LIBRARY_STRING, "Plain string. Values are passed through untouched, leaving date handling to the consumer.");
165+
dateOptions.put(DATE_LIBRARY_TEMPORAL, "Native Temporal. `format: date` is mapped to Temporal.PlainDate and `format: date-time` is mapped to Temporal.Instant and (de)serialized by the runtime.");
157166
dateLibraryOption.setEnum(dateOptions);
158167
this.cliOptions.add(dateLibraryOption);
159168
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()));
@@ -349,24 +358,29 @@ public void processOpts() {
349358

350359
// `date` needs the model (de)serialization to convert with, which
351360
// withoutRuntimeChecks removes: the raw string would just be cast to Date.
352-
if (withoutRuntimeChecks && DATE_LIBRARY_DATE.equals(this.dateLibrary)) {
361+
if (withoutRuntimeChecks && (DATE_LIBRARY_DATE.equals(this.dateLibrary) || DATE_LIBRARY_TEMPORAL.equals(this.dateLibrary))) {
353362
if (additionalProperties.containsKey(DATE_LIBRARY)) {
354363
LOGGER.warn("{}={} is not compatible with {}=true; falling back to {}={}.",
355-
DATE_LIBRARY, DATE_LIBRARY_DATE, WITHOUT_RUNTIME_CHECKS, DATE_LIBRARY, DATE_LIBRARY_STRING);
364+
DATE_LIBRARY, this.dateLibrary, WITHOUT_RUNTIME_CHECKS, DATE_LIBRARY, DATE_LIBRARY_STRING);
356365
}
357366
this.dateLibrary = DATE_LIBRARY_STRING;
358367
}
359368

360369
if (DATE_LIBRARY_DATE.equals(this.dateLibrary)) {
361370
typeMapping.put(DATE_TYPE, TS_DATE_TYPE);
362371
typeMapping.put(DATE_TIME_TYPE, TS_DATE_TYPE);
372+
} else if (DATE_LIBRARY_TEMPORAL.equals(this.dateLibrary)) {
373+
typeMapping.put(DATE_TYPE, TS_TEMPORAL_PLAIN_DATE_TYPE);
374+
typeMapping.put(DATE_TIME_TYPE, TS_TEMPORAL_INSTANT_TYPE);
363375
} else {
364376
typeMapping.put(DATE_TYPE, "string");
365377
typeMapping.put(DATE_TIME_TYPE, "string");
366378
}
367379
additionalProperties.put(DATE_LIBRARY, this.dateLibrary);
368380
// Mustache cannot compare strings, so expose the selected library as a flag.
369381
additionalProperties.put("isDateLibraryDate", DATE_LIBRARY_DATE.equals(this.dateLibrary));
382+
additionalProperties.put("isDateLibraryString", DATE_LIBRARY_STRING.equals(this.dateLibrary));
383+
additionalProperties.put("isDateLibraryTemporal", DATE_LIBRARY_TEMPORAL.equals(this.dateLibrary));
370384

371385
if (additionalProperties.containsKey(SAGAS_AND_RECORDS)) {
372386
this.setSagasAndRecords(convertPropertyToBoolean(SAGAS_AND_RECORDS));
@@ -2125,10 +2139,10 @@ protected String getLicenseNameDefaultValue() {
21252139
}
21262140

21272141
private static boolean isDateType(String dataType) {
2128-
return TS_DATE_TYPE.equals(dataType);
2142+
return TS_DATE_TYPE.equals(dataType) || TS_TEMPORAL_PLAIN_DATE_TYPE.equals(dataType);
21292143
}
21302144

21312145
private static boolean isDateTimeType(String dataType) {
2132-
return TS_DATE_TYPE.equals(dataType);
2146+
return TS_DATE_TYPE.equals(dataType) || TS_TEMPORAL_INSTANT_TYPE.equals(dataType);
21332147
}
21342148
}

‎modules/openapi-generator/src/main/resources/typescript-fetch/apis.mustache‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,16 +307,28 @@ export class {{classname}} extends runtime.BaseAPI {
307307
let urlPath = `{{{path}}}`;
308308
{{#pathParams}}
309309
{{#isDateTimeType}}
310+
{{#isDateLibraryTemporal}}
311+
if (requestParameters['{{paramName}}'] instanceof Temporal.Instant) {
312+
urlPath = urlPath.replace({{=<< >>=}}'{<<baseName>>}'<<={{ }}=>>, encodeURIComponent(runtime.serializeDateTime(requestParameters['{{paramName}}'])));
313+
{{/isDateLibraryTemporal}}
314+
{{^isDateLibraryTemporal}}
310315
if (requestParameters['{{paramName}}'] instanceof Date) {
311316
urlPath = urlPath.replace({{=<< >>=}}'{<<baseName>>}'<<={{ }}=>>, encodeURIComponent(runtime.serializeDateTime(requestParameters['{{paramName}}'])));
317+
{{/isDateLibraryTemporal}}
312318
} else {
313319
urlPath = urlPath.replace({{=<< >>=}}'{<<baseName>>}'<<={{ }}=>>, encodeURIComponent(String(requestParameters['{{paramName}}'])));
314320
}
315321
{{/isDateTimeType}}
316322
{{^isDateTimeType}}
317323
{{#isDateType}}
324+
{{#isDateLibraryTemporal}}
325+
if (requestParameters['{{paramName}}'] instanceof Temporal.PlainDate) {
326+
urlPath = urlPath.replace({{=<< >>=}}'{<<baseName>>}'<<={{ }}=>>, encodeURIComponent(runtime.serializeDate(requestParameters['{{paramName}}'])));
327+
{{/isDateLibraryTemporal}}
328+
{{^isDateLibraryTemporal}}
318329
if (requestParameters['{{paramName}}'] instanceof Date) {
319330
urlPath = urlPath.replace({{=<< >>=}}'{<<baseName>>}'<<={{ }}=>>, encodeURIComponent(runtime.serializeDate(requestParameters['{{paramName}}'])));
331+
{{/isDateLibraryTemporal}}
320332
} else {
321333
urlPath = urlPath.replace({{=<< >>=}}'{<<baseName>>}'<<={{ }}=>>, encodeURIComponent(String(requestParameters['{{paramName}}'])));
322334
}

‎modules/openapi-generator/src/main/resources/typescript-fetch/modelGeneric.mustache‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mapValues{{#isDateLibraryDate}}{{#vendorExtensions.x-hasDateVars}}, parseDate, parseDateTime, serializeDate, serializeDateTime{{/vendorExtensions.x-hasDateVars}}{{/isDateLibraryDate}} } from '../runtime{{importFileExtension}}';
1+
import { mapValues{{^isDateLibraryString}}{{#vendorExtensions.x-hasDateVars}}, parseDate, parseDateTime, serializeDate, serializeDateTime{{/vendorExtensions.x-hasDateVars}}{{/isDateLibraryString}} } from '../runtime{{importFileExtension}}';
22
{{#hasImports}}
33
{{#tsImports}}
44
import type { {{{classname}}} } from './{{filename}}{{importFileExtension}}';

‎modules/openapi-generator/src/main/resources/typescript-fetch/modelOneOf.mustache‎

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
{{#isDateLibraryDate}}
1+
{{^isDateLibraryString}}
22
import { parseDate, parseDateTime, serializeDate, serializeDateTime } from '../runtime{{importFileExtension}}';
3-
{{/isDateLibraryDate}}
3+
{{/isDateLibraryString}}
44
{{#hasImports}}
55
{{#oneOfArrays}}
66
import type { {{{.}}} } from './{{.}}{{importFileExtension}}';
@@ -72,16 +72,30 @@ export function {{classname}}FromJSONTyped(json: any, ignoreDiscriminator: boole
7272
{{#items}}
7373
{{#isDateType}}
7474
if (Array.isArray(json)) {
75+
{{#isDateLibraryTemporal}}
76+
try {
77+
return json.map(value => parseDate(value));
78+
} catch {}
79+
{{/isDateLibraryTemporal}}
80+
{{^isDateLibraryTemporal}}
7581
if (json.every(item => !(isNaN(parseDate(item).getTime())))) {
7682
return json.map(value => parseDate(value));
7783
}
84+
{{/isDateLibraryTemporal}}
7885
}
7986
{{/isDateType}}
8087
{{#isDateTimeType}}
8188
if (Array.isArray(json)) {
89+
{{#isDateLibraryTemporal}}
90+
try {
91+
return json.map(value => parseDateTime(value));
92+
} catch {}
93+
{{/isDateLibraryTemporal}}
94+
{{^isDateLibraryTemporal}}
8295
if (json.every(item => !(isNaN(parseDateTime(item).getTime())))) {
8396
return json.map(value => parseDateTime(value));
8497
}
98+
{{/isDateLibraryTemporal}}
8599
}
86100
{{/isDateTimeType}}
87101
{{#isNumeric}}
@@ -118,15 +132,29 @@ export function {{classname}}FromJSONTyped(json: any, ignoreDiscriminator: boole
118132
{{#oneOfPrimitives}}
119133
{{^isArray}}
120134
{{#isDateType}}
135+
{{#isDateLibraryTemporal}}
136+
try {
137+
return {{^required}}json == null ? undefined : {{/required}}({{#required}}{{#isNullable}}json == null ? null : {{/isNullable}}{{/required}}parseDate(json));
138+
} catch {}
139+
{{/isDateLibraryTemporal}}
140+
{{^isDateLibraryTemporal}}
121141
if (!(isNaN(parseDate(json).getTime()))) {
122142
return {{^required}}json == null ? undefined : {{/required}}({{#required}}{{#isNullable}}json == null ? null : {{/isNullable}}{{/required}}parseDate(json));
123143
}
144+
{{/isDateLibraryTemporal}}
124145
{{/isDateType}}
125146
{{^isDateType}}
126147
{{#isDateTimeType}}
148+
{{#isDateLibraryTemporal}}
149+
try {
150+
return {{^required}}json == null ? undefined : {{/required}}({{#required}}{{#isNullable}}json == null ? null : {{/isNullable}}{{/required}}parseDateTime(json));
151+
} catch {}
152+
{{/isDateLibraryTemporal}}
153+
{{^isDateLibraryTemporal}}
127154
if (!(isNaN(parseDateTime(json).getTime()))) {
128155
return {{^required}}json == null ? undefined : {{/required}}({{#required}}{{#isNullable}}json == null ? null : {{/isNullable}}{{/required}}parseDateTime(json));
129156
}
157+
{{/isDateLibraryTemporal}}
130158
{{/isDateTimeType}}
131159
{{/isDateType}}
132160
{{#isNumeric}}
@@ -197,16 +225,30 @@ export function {{classname}}ToJSONTyped(value?: {{classname}} | null, ignoreDis
197225
{{#items}}
198226
{{#isDateType}}
199227
if (Array.isArray(value)) {
228+
{{#isDateLibraryTemporal}}
229+
if (value.every(item => item instanceof Temporal.PlainDate)) {
230+
return value.map(value => serializeDate(value));
231+
}
232+
{{/isDateLibraryTemporal}}
233+
{{^isDateLibraryTemporal}}
200234
if (value.every(item => item instanceof Date)) {
201235
return value.map(value => serializeDate(value));
202236
}
237+
{{/isDateLibraryTemporal}}
203238
}
204239
{{/isDateType}}
205240
{{#isDateTimeType}}
206241
if (Array.isArray(value)) {
242+
{{#isDateLibraryTemporal}}
243+
if (value.every(item => item instanceof Temporal.Instant)) {
244+
return value.map(item => serializeDateTime(item));
245+
}
246+
{{/isDateLibraryTemporal}}
247+
{{^isDateLibraryTemporal}}
207248
if (value.every(item => item instanceof Date)) {
208249
return value.map(item => serializeDateTime(item));
209250
}
251+
{{/isDateLibraryTemporal}}
210252
}
211253
{{/isDateTimeType}}
212254
{{#isNumeric}}
@@ -243,14 +285,28 @@ export function {{classname}}ToJSONTyped(value?: {{classname}} | null, ignoreDis
243285
{{#oneOfPrimitives}}
244286
{{^isArray}}
245287
{{#isDateType}}
288+
{{#isDateLibraryTemporal}}
289+
if (value instanceof Temporal.PlainDate) {
290+
return (serializeDate(value{{#isNullable}} as any{{/isNullable}}));
291+
}
292+
{{/isDateLibraryTemporal}}
293+
{{^isDateLibraryTemporal}}
246294
if (value instanceof Date) {
247295
return (serializeDate(value{{#isNullable}} as any{{/isNullable}}));
248296
}
297+
{{/isDateLibraryTemporal}}
249298
{{/isDateType}}
250299
{{#isDateTimeType}}
300+
{{#isDateLibraryTemporal}}
301+
if (value instanceof Temporal.Instant) {
302+
return {{^required}}{{#isNullable}}value === null ? null : {{/isNullable}}{{^isNullable}}value == null ? undefined : {{/isNullable}}{{/required}}(serializeDateTime(value{{#isNullable}} as any{{/isNullable}}));
303+
}
304+
{{/isDateLibraryTemporal}}
305+
{{^isDateLibraryTemporal}}
251306
if (value instanceof Date) {
252307
return {{^required}}{{#isNullable}}value === null ? null : {{/isNullable}}{{^isNullable}}value == null ? undefined : {{/isNullable}}{{/required}}(serializeDateTime(value{{#isNullable}} as any{{/isNullable}}));
253308
}
309+
{{/isDateLibraryTemporal}}
254310
{{/isDateTimeType}}
255311
{{#isNumeric}}
256312
if (typeof value === 'number'{{#isEnum}} && ({{#allowableValues}}{{#values}}value === {{.}}{{^-last}} || {{/-last}}{{/values}}{{/allowableValues}}){{/isEnum}}) {

‎modules/openapi-generator/src/main/resources/typescript-fetch/runtime.mustache‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,9 +350,19 @@ function querystringSingleKey(key: string, value: string | number | null | undef
350350
const valueAsArray = Array.from(value);
351351
return querystringSingleKey(key, valueAsArray, keyPrefix);
352352
}
353+
{{#isDateLibraryTemporal}}
354+
if (value instanceof Temporal.Instant) {
355+
return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`;
356+
}
357+
if (value instanceof Temporal.PlainDate) {
358+
return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDate(value))}`;
359+
}
360+
{{/isDateLibraryTemporal}}
361+
{{^isDateLibraryTemporal}}
353362
if (value instanceof Date) {
354363
return `${encodeURIComponent(fullKey)}=${encodeURIComponent(serializeDateTime(value))}`;
355364
}
365+
{{/isDateLibraryTemporal}}
356366
if (value instanceof Object) {
357367
return querystring(value as HTTPQuery, fullKey);
358368
}
@@ -364,6 +374,30 @@ export function exists(json: any, key: string) {
364374
return value !== null && value !== undefined;
365375
}
366376

377+
{{#isDateLibraryTemporal}}
378+
export function serializeDateTime(value: Temporal.Instant): string {
379+
return value.toString();
380+
}
381+
382+
export function serializeDate(value: Temporal.PlainDate): string {
383+
return value.toString({ calendarName: "never" });
384+
}
385+
386+
export function parseDateTime(value: Temporal.Instant | string): Temporal.Instant {
387+
if (value instanceof Temporal.Instant) {
388+
return value;
389+
}
390+
return Temporal.Instant.from(value);
391+
}
392+
393+
export function parseDate(value: Temporal.PlainDate | string): Temporal.PlainDate {
394+
if (value instanceof Temporal.PlainDate) {
395+
return value;
396+
}
397+
return Temporal.PlainDate.from(value);
398+
}
399+
{{/isDateLibraryTemporal}}
400+
{{^isDateLibraryTemporal}}
367401
/**
368402
* Every generated date call site routes through these.
369403
*
@@ -416,6 +450,7 @@ export function parseDateTime(value: any): Date {
416450
return new Date(value);
417451
}
418452
{{/isDateLibraryDate}}
453+
{{/isDateLibraryTemporal}}
419454

420455
{{^withoutRuntimeChecks}}
421456
export function mapValues(data: any, fn: (item: any) => any) {

0 commit comments

Comments
 (0)