Skip to content

Commit

Permalink
Merge branch 'release/11.0.0-rc' into bugfix/multi-tag-support-11
Browse files Browse the repository at this point in the history
  • Loading branch information
kpanot authored Jul 9, 2024
2 parents 8f57a71 + 3721894 commit 6c2ab23
Show file tree
Hide file tree
Showing 9 changed files with 202 additions and 78 deletions.
46 changes: 46 additions & 0 deletions migration-guides/11.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,49 @@
## @o3r/schematics

- `NodePackageNgAddTask` has been removed. `setupDependencies` should be used instead

- The [`stringifyDate` CLI option](https://www.npmjs.com/package/@ama-sdk/schematics#dates) is now enabled by default. If
you want to use the `Date` format, you will need to set it to `false` via the command line:

```shell
# with yarn package manager
yarn run schematics @ama-sdk/schematics:typescript-core --global-property stringifyDate=false
# with npm package manager
npm exec schematics @ama-sdk/schematics:typescript-core -- --global-property stringifyDate=false
```

or by disabling it for your generator in your `openapitools.json` file:
```json5
{
"$schema": "https://raw.githubusercontent.com/OpenAPITools/openapi-generator-cli/master/apps/generator-cli/src/config.schema.json",
"generator-cli": {
"version": "7.4.0", // Version of the Codegen downloaded by the cli - updated via the
"storageDir": ".openapi-generator",
"generators": {
"my-generator": {
"generatorName": "typescriptFetch",
"output": ".",
"inputSpec": "spec-path.(yaml|json)",
"globalProperty": {
"stringifyDate": false
}
}
}
}
}
```

- Prior to version 11, the default types for date and date-time objects were respectively `utils.Date` and `utils.DateTime`.
Those two types take the date from the API response and remove their timezone to consider the date as if it were in the
timezone of the user's machine (on a computer on UTC, the 01/01/2000 12:00 in UTC+3 would become 01/01/2000 12:00 UTC+0).
If a date had to be expressed in its original timezone, the date specification required an extra `x-date-timezone` vendor.
As of version 11, the default behavior is now to keep the timezone of the API response for date-time objects. They
would be revived as `Date` or kept as `string` depending on the value of the `stringifyDate` option.
The removal the timezone is now an 'opt-in' mechanism triggered by the `x-local-timezone` vendor.

> [!NOTE]
> This change only impacts `date-time` format. `Date` format will still be revived as `utils.Date` objects as the timezone
> should always be ignored for date object (since there is no concept of time).
> [!NOTE]
> You can revert to the previous behavior thanks to the global property `useLegacyDateExtension`
2 changes: 1 addition & 1 deletion packages/@ama-sdk/core/src/fwk/api.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ export function getResponseReviver<T>(revivers: { [statusCode: number]: ReviverT
if (typeof revivers === 'function' || typeof revivers === 'undefined') {
return revivers;
}
if (response.status && revivers[response.status]) {
if (response.status && Object.keys(revivers).indexOf(`${response.status}`) > -1) {
return revivers[response.status];
}
if (options?.disableFallback) {
Expand Down
9 changes: 7 additions & 2 deletions packages/@ama-sdk/create/src/index.it.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ describe('Create new sdk command', () => {
expect(() =>
packageManagerCreate({
script: '@ama-sdk',
args: ['typescript', sdkPackageName, '--package-manager', packageManager, '--spec-path', path.join(sdkFolderPath, 'swagger-spec-with-date.yml')]
args: [
'typescript',
sdkPackageName,
'--package-manager', packageManager,
'--spec-path', path.join(sdkFolderPath, 'swagger-spec-with-date.yml')]
}, execAppOptions)
).not.toThrow();
expect(() => packageManagerRun({script: 'build'}, { ...execAppOptions, cwd: sdkPackagePath })).not.toThrow();
Expand All @@ -93,7 +97,8 @@ describe('Create new sdk command', () => {
}, execAppOptions)
).not.toThrow();
expect(() => packageManagerRun({script: 'build'}, { ...execAppOptions, cwd: sdkPackagePath })).not.toThrow();
expect(existsSync(path.join(sdkPackagePath, 'src', 'models', 'base', 'pet', 'pet.reviver.ts'))).toBeTruthy();
expect(existsSync(path.join(sdkPackagePath, 'src', 'models', 'base', 'pet', 'pet.ts'))).toBeTruthy();
expect(existsSync(path.join(sdkPackagePath, 'src', 'models', 'base', 'pet', 'pet.reviver.ts'))).toBeFalsy();
expect(() => packageManagerRun({script: 'spec:upgrade'}, { ...execAppOptions, cwd: sdkPackagePath })).not.toThrow();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ definitions:
properties:
nextMeetingAtTheVet:
type: string
format: date
format: date-time
x-local-timezone: true
friends:
type: object
allOf:
Expand Down
14 changes: 7 additions & 7 deletions packages/@ama-sdk/schematics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,16 +127,16 @@ Please note that revivers are generated for SDKs that use:

If your specification file includes dates, there are multiple options for the generation of your SDK involving the global property option `stringifyDate`:

- By default, the option `stringifyDate` is false so dates will be generated as either `utils.Date`, `utils.DateTime`, or `Date`.
- By default, the option `stringifyDate` is set to `true`. Set it to `false` if you want date-time objects to be generated
as `Date` and date objects to be generated as `utils.Date`.
For more information related to these types, check out this [documentation](https://github.com/AmadeusITGroup/otter/tree/main/packages/%40ama-sdk/schematics/schematics/typescript/shell/templates/base#manage-dates).
- To leave the handling of the timezone to the application and generate the dates as `string` types, the option `stringifyDate` can be set to true. <br>
This can be done by adding `--global-property stringifyDate` to the generator command.
- If an existing SDK contains stringified dates that need to be reverted to their expected formats, you can regenerate the SDK by removing the `stringifyDate` option from the global properties (since it is false by default).
This can be done by adding `--global-property stringifyDate=false` to the generator command or by adding the global property
to the `openapitools.json`.

Example to stringify dates:
Example to use `Date`:

```shell
yarn schematics @ama-sdk/schematics:typescript-core --spec-path ./swagger-spec.yaml --global-property stringifyDate
yarn schematics @ama-sdk/schematics:typescript-core --spec-path ./swagger-spec.yaml --global-property stringifyDate=false
```

##### Extensible models
Expand Down Expand Up @@ -214,7 +214,7 @@ described global properties `stringifyDate` and `allowModelExtension`:
"output": ".",
"inputSpec": "./openapi-spec.yaml", // or "./openapi-spec.json" according to the specification format
"globalProperty": {
"stringifyDate": true,
"stringifyDate": false,
"allowModelExtension": true
}
}
Expand Down
20 changes: 19 additions & 1 deletion packages/@ama-sdk/schematics/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,25 @@
"executor": "nx:run-script",
"options": {
"script": "build:builders"
}
},
"outputs": [
"{projectRoot}/dist/src/**/*.js",
"{projectRoot}/dist/src/**/*.d.ts",
"{projectRoot}/dist/src/**/*.d.ts.map",
"{projectRoot}/dist/builders/package.json",
"{projectRoot}/dist/builders/**/*.js",
"{projectRoot}/dist/builders/**/*.d.ts",
"{projectRoot}/dist/builders/**/*.d.ts.map",
"{projectRoot}/dist/schematics/package.json",
"{projectRoot}/dist/schematics/**/*.js",
"{projectRoot}/dist/schematics/**/*.d.ts",
"{projectRoot}/dist/schematics/**/*.d.ts.map",
"{projectRoot}/dist/middlewares/package.json",
"{projectRoot}/dist/middlewares/**/*.js",
"{projectRoot}/dist/middlewares/**/*.d.ts",
"{projectRoot}/dist/middlewares/**/*.d.ts.map",
"{projectRoot}/build/.tsbuildinfo.builders"
]
},
"build-cli": {
"executor": "nx:run-script",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public abstract class AbstractTypeScriptClientCodegen extends DefaultCodegen imp

private final boolean stringifyDate;
private final boolean allowModelExtension;
private final boolean useLegacyDateExtension;

public AbstractTypeScriptClientCodegen() {
super();
Expand Down Expand Up @@ -110,11 +111,13 @@ public AbstractTypeScriptClientCodegen() {
typeMapping.put("integer", "number");
typeMapping.put("Map", "any");
String allowModelExtensionString = GlobalSettings.getProperty("allowModelExtension");
String useLegacyDateExtensionString = GlobalSettings.getProperty("useLegacyDateExtension");
useLegacyDateExtension = useLegacyDateExtensionString != null ? !"false".equalsIgnoreCase(useLegacyDateExtensionString) : false;
allowModelExtension = allowModelExtensionString != null ? !"false".equalsIgnoreCase(allowModelExtensionString) : false;
String stringifyDateString = GlobalSettings.getProperty("stringifyDate");
stringifyDate = stringifyDateString != null ? !"false".equalsIgnoreCase(stringifyDateString) : false;
typeMapping.put("DateTime", stringifyDate ? "string" : "utils.DateTime");
typeMapping.put("Date", stringifyDate ? "string" : "utils.Date");
stringifyDate = stringifyDateString != null ? !"false".equalsIgnoreCase(stringifyDateString) : true;
typeMapping.put("DateTime", useLegacyDateExtension ? "utils.DateTime" : getDateTimeStandardTime(stringifyDate));
typeMapping.put("Date", useLegacyDateExtension ? "utils.Date" : getDateType(stringifyDate));
//TODO binary should be mapped to byte array
// mapped to String as a workaround
typeMapping.put("binary", "string");
Expand Down Expand Up @@ -444,12 +447,6 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
}
}

if (property != null && !stringifyDate) {
if (Boolean.TRUE.equals(property.isDate) || Boolean.TRUE.equals(property.isDateTime)) {
property.isPrimitiveType = false;
}
}

if (property.isEnum) {
List<String> allowableValues = (List) property.allowableValues.get("values");
List<String> sanitizedAllowableValues = allowableValues;
Expand Down Expand Up @@ -511,16 +508,105 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
throw new IllegalArgumentException("error in " + property.baseName + ", x-map-name should only apply to a "
+ "list container.");
}
if (property.isDate || property.isDateTime) {
String dateDataTypeOverride = this.getDateDataTypeOverride(property.name, property.isDateTime, property.vendorExtensions);
if (dateDataTypeOverride != null) {
property.dataType = dateDataTypeOverride;
property.datatypeWithEnum = dateDataTypeOverride;
property.baseType = dateDataTypeOverride;
}

// If the x-date-timezone is present in the specification, replace utils.Date with Date
if (("utils.Date".equals(property.dataType) || "utils.DateTime".equals(property.dataType)) && property.vendorExtensions.containsKey("x-date-timezone")) {
property.dataType = "Date";
property.datatypeWithEnum = "Date";
property.baseType = "Date";
if (property != null && property.dataType != null && property.dataType.matches("^(Date|utils.Date|utils.DateTime)$")) {
property.isPrimitiveType = false;
}
}
property.vendorExtensions.put("x-exposed-classname", model.classname);
}

/**
* Compute date type for date or date-type object considering local timezone offset.
*
* @param name
* @param isDateTime
* @param extensions
*/
private String getDateDataTypeOverride(String name, boolean isDateTime, Map<String, Object> extensions) {
boolean activateLocalDateTime = extensions.containsKey("x-local-timezone");
boolean deactivateLocalDateTime = extensions.containsKey("x-date-timezone");

if (activateLocalDateTime && deactivateLocalDateTime) {
throw new IllegalArgumentException("The 'x-local-timezone' and the 'x-date-timezone' vendor are not compatible.");
} else if (deactivateLocalDateTime && !useLegacyDateExtension) {
throw new IllegalArgumentException("'x-date-timezone' is deprecated and conflicts with the 'x-local-timezone' vendor." +
" Please check out the documentation and migrate to the 'x-local-timezone' model: https://github.com/AmadeusITGroup/otter/blob/main/migration-guides/11.0.md."
);
} else if (activateLocalDateTime && useLegacyDateExtension) {
LOGGER.error("Unsupported 'x-local-timezone' vendor found for property " + name + " while using the " +
"'useLegacyDateExtension'. The vendor will be ignored in this scenario, please deactivate the " +
"'useLegacyDateExtension if you want to use this vendor.");
activateLocalDateTime = false;
}

if (isDateTime) {
if (activateLocalDateTime) {
return "utils.DateTime";
}
if (deactivateLocalDateTime) {
return this.getDateTimeStandardTime(stringifyDate);
}
return typeMapping.get("DateTime");
}

if (activateLocalDateTime) {
LOGGER.error(name + " has the 'x-local-timezone' vendor which is only compatible for date-time format. This will " +
"be ignored and normal logic will apply.");
return null;
}
if (deactivateLocalDateTime) {
return this.getDateType(stringifyDate);
}

return typeMapping.get("Date");
}

/**
* @inherit
*/
public void postProcessParameter(CodegenParameter parameter) {
if (parameter.isDate || parameter.isDateTime) {
String dateDataTypeOverride = this.getDateDataTypeOverride(parameter.paramName, parameter.isDateTime, parameter.vendorExtensions);
if (dateDataTypeOverride != null) {
parameter.dataType = dateDataTypeOverride;
parameter.datatypeWithEnum = dateDataTypeOverride;
parameter.baseType = dateDataTypeOverride;
}

if (parameter != null && parameter.dataType != null && parameter.dataType.matches("^(Date|utils.Date|utils.DateTime)$")) {
parameter.isPrimitiveType = false;
}
}
}

/**
* Get type for date-time object without any modification on the timezone.
*
* @param isDateStringified
* @return
*/
private String getDateTimeStandardTime(boolean isDateStringified) {
return isDateStringified ? "string" : "Date";
}

/**
* Get type for date object.
*
* @param isDateStringified
* @return
*/
private String getDateType(boolean isDateStringified) {
return isDateStringified ? "string" : "utils.Date";
}

/**
* As we do not want to modify Swagger's generator, we need to remove the package from the imports.
* Also, extracts additional imports from vendor extensions (used for dictionaries);
Expand Down Expand Up @@ -558,6 +644,7 @@ protected void addImport(CodegenModel m, String type) {

/**
* Post process the import of a ModelsMap, make sure to include the field and the subtypes with their revivers
*
* @param objs
* @return
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,16 @@ For example, you want to be able to display that the flight is in X hours.
You will need to compute this information with the two timezones -- the airport's and the user's.

### Solution proposed to remove the timezone: utils.DateTime
We have introduced the utils.Date object to replace the Date implementation and ignore the timezone.
As we need to get rid of the timezones more often than not, this will be our default behavior.
By default, the Date models are replaced with utils.Date.
The Otter framework has introduced the `utils.Date` and `utils.DateTime` objects to replace the `Date` implementation and convert the date returned by the API as if it were in the
timezone of the user.

When we need to keep the timezone information, we create a new field directly in the SDK.
As this field does not exist in the specification, it will not be part of the base model but of the core model instead.
Dates can be generated as `utils.Date` or `string` depending on the value of the `stringifyDate` option. This ensures that the timezone will not impact the date.
In the case of `date-time` objects, the default type used is the native `string` and `Date` type depending on the `stringifyDate` option value.

If you want to generate a date-time using `utils.DateTime`, you can do it at property level thanks to the `x-local-timezone` vendor.

If you need to keep the timezone information, extend the model and create a new field directly in the SDK.
As this field does not exist in the specification, it will not be part of the base model but of the core model instead (the first one being completely generated from the API specifications).

Simple example:
```yaml
Expand All @@ -140,6 +144,8 @@ Simple example:
properties:
departureDateTime:
type: string
x-local-timezone: true
description: If this vendor extension is present send dates without their timezone
format: date-time
```
Base model generated
Expand Down Expand Up @@ -197,23 +203,3 @@ export * from './flight/index';

You can now use departureDateTimeConsideringTimezone to access the timezone information.
See [utils.Date](https://github.com/AmadeusITGroup/otter/blob/main/packages/%40ama-sdk/core/src/fwk/date.ts) for more information.

### How to keep the timezone (prevent Date replacement with utils.Date)

In order to add the timezone to your timestamp property you can add the x-date-timezone extension in your yaml, for example:

```yaml
properties:
timestamp:
title: timestamp
description: >-
Timestamp when event is triggered. UTC time (server time), with a
format similar to yyyy-MM-ddThh:mm:ss.sTZD. Refer to the pattern
type: string
format: date-time
x-date-timezone:
description: If this vendor extension is present send dates with the timezone
pattern: >-
^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{1,3}([Z]|([+][0-9]{2}:?[0-9]{2}$))
example: '2013-12-31T19:20:30.45+01:00'
```
Loading

0 comments on commit 6c2ab23

Please sign in to comment.