Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: otter sdk training - model extension #2411

Open
wants to merge 1 commit into
base: feat/otter-training
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions apps/showcase/src/assets/trainings/sdk/program.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
],
"mode": "interactive",
"commands": [
"npm install --legacy-peer-deps --ignore-scripts --force",
"npm install --legacy-peer-deps --ignore-scripts --no-audit --prefer-dedupe",
"npm run ng run sdk:build",
"npm run ng run tutorial-app:serve"
]
Expand Down Expand Up @@ -95,7 +95,7 @@
],
"mode": "interactive",
"commands": [
"npm install --legacy-peer-deps --ignore-scripts --force",
"npm install --legacy-peer-deps --ignore-scripts --no-audit --prefer-dedupe",
"npm run ng run app:serve"
]
}
Expand All @@ -111,6 +111,10 @@
"path": ".",
"contentUrl": "./shared/monorepo-template.json"
},
{
"path": ".",
"contentUrl": "./shared/training-sdk-app.json"
},
{
"path": "./libs/sdk/src",
"contentUrl": "@o3r-training/training-sdk/structure/src.json"
Expand All @@ -128,7 +132,7 @@
],
"mode": "interactive",
"commands": [
"npm install --legacy-peer-deps --ignore-scripts --force",
"npm install --legacy-peer-deps --ignore-scripts --no-audit --prefer-dedupe",
"npm run ng run tutorial-app:serve"
]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
fpaul-1A marked this conversation as resolved.
Show resolved Hide resolved
"$schema": "../../../../../schemas/webcontainer-file-system-tree.schema.json",
"fileSystemTree": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is rather specific, this will not be shared with all the exercises. It should probably be generated from the exercise folder.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually thought it could be used on other exercises, it's not specific to model extension, it's just an app calling the dummy api with mocks

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have it versioned then in a similar fashion as the sdk? It will be easier to update the content and the dependencies

"apps": {
"directory": {
"tutorial-app": {
"directory": {
"src": {
"directory": {
"app": {
"directory": {
"app.component.html": {
"file": {
"contents": "Revived flight:\r\n<pre>{{flight() | json }}</pre>"
}
},
"app.component.ts": {
"file": {
"contents": "import { Component, inject, signal } from '@angular/core';\nimport { JsonPipe } from '@angular/common';\nimport { RouterOutlet } from '@angular/router';\nimport { DummyApi, Flight } from 'sdk';\n\n@Component({\n selector: 'app-root',\n standalone: true,\n imports: [JsonPipe, RouterOutlet],\n templateUrl: './app.component.html',\n styleUrl: './app.component.scss'\n})\nexport class AppComponent {\n /** Title of the application */\n public title = 'tutorial-app';\n\n public readonly dummyApi = inject(DummyApi);\n\n public readonly flight = signal<Flight | undefined>(undefined);\n\n constructor() {\n this.loadDummyData();\n }\n\n async loadDummyData() {\n const dummyData = await this.dummyApi.dummyGet({});\n this.flight.set(dummyData);\n }\n}\n"
}
},
"app.config.ts": {
"file": {
"contents": "import { ApiFetchClient } from '@ama-sdk/client-fetch';\nimport { MockInterceptRequest, SequentialMockAdapter } from '@ama-sdk/core';\nimport { ApplicationConfig, provideZoneChangeDetection, importProvidersFrom } from '@angular/core';\nimport { BrowserAnimationsModule } from '@angular/platform-browser/animations';\nimport { provideRouter } from '@angular/router';\nimport { prefersReducedMotion } from '@o3r/application';\nimport { Serializer } from '@o3r/core';\nimport { ConsoleLogger, Logger, LOGGER_CLIENT_TOKEN, LoggerService } from '@o3r/logger';\nimport { StorageSync } from '@o3r/store-sync';\nimport { EffectsModule } from '@ngrx/effects';\nimport { RuntimeChecks, StoreModule } from '@ngrx/store';\nimport { DummyApi } from 'sdk';\nimport { OPERATION_ADAPTER } from 'sdk/spec';\nimport { routes } from './app.routes';\nimport { environment, additionalModules } from '../environments/environment';\n\nconst localStorageStates: Record<string, Serializer<any>>[] = [/* Store to register in local storage */];\nconst storageSync = new StorageSync({\n keys: localStorageStates, rehydrate: true\n});\n\nconst rootReducers = {\n \n};\n\nconst metaReducers = [storageSync.localStorageSync()];\nconst runtimeChecks: Partial<RuntimeChecks> = {\n strictActionImmutability: false,\n strictActionSerializability: false,\n strictActionTypeUniqueness: !environment.production,\n strictActionWithinNgZone: !environment.production,\n strictStateImmutability: !environment.production,\n strictStateSerializability: false\n};\n\nfunction dummyApiFactory(logger: Logger) {\n const apiConfig = new ApiFetchClient(\n {\n basePath: 'http://localhost:3000',\n requestPlugins: [\n new MockInterceptRequest({\n adapter: new SequentialMockAdapter(\n OPERATION_ADAPTER,\n {\n '/dummy_get': [{\n mockData: {\n originLocationCode: 'PAR',\n destinationLocationCode: 'NYC'\n }\n }]\n }\n )\n })\n ],\n fetchPlugins: [],\n logger\n }\n );\n return new DummyApi(apiConfig);\n}\n\nexport const appConfig: ApplicationConfig = {\n providers: [\n provideZoneChangeDetection({ eventCoalescing: true }),\n provideRouter(routes),\n importProvidersFrom(EffectsModule.forRoot([])),\n importProvidersFrom(StoreModule.forRoot(rootReducers, {metaReducers, runtimeChecks})),\n importProvidersFrom(additionalModules),\n importProvidersFrom(BrowserAnimationsModule.withConfig({disableAnimations: prefersReducedMotion()})),\n {provide: LOGGER_CLIENT_TOKEN, useValue: new ConsoleLogger()},\n {provide: DummyApi, useFactory: dummyApiFactory, deps: [LoggerService]}\n ]\n};\n"
}
}
}
}
}
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/* TODO Export your extended model and reviver instead of the original ones */
export type { Flight } from './flight';
export { reviveFlight } from './flight.reviver';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* TODO Modify the implementation of reviveFlightFactory to call `baseRevive` and add an extra id */
import type { reviveFlight } from '../../base/flight/flight.reviver';

/**
* Extended reviver for Flight
*
* @param baseRevive
*/
export function reviveFlightFactory<R extends typeof reviveFlight>(baseRevive: R) {
return baseRevive;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/* TODO create the type FlightCoreIfy which extends Flight, imported from the ../base folder */
cpaulve-1A marked this conversation as resolved.
Show resolved Hide resolved
/* Add an extra field `id: string` */
sdo-1A marked this conversation as resolved.
Show resolved Hide resolved

/**
* Extended type for Flight
*/
export type FlightCoreIfy = {

};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './flight';
export * from './flight.reviver';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Export your core models here
export * from './flight';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './base';
export * from './core';
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
### Objective
Let's continue with the use case of the previous exercise.\
In order to keep track of the user's current booking, it would be useful to generate an ID.\
To do this, we are going to create a new model which extends the previously generated `Flight` type.

### Exercise

#### Check out the base model
Before proceeding with the extension of the model, let's take a moment to review what is in the base model.
In the folder `libs/sdk/src/models/base/flight`, there are 3 files:
- `flight.ts` is the base model definition
- `flight.reviver.ts` is the reviver of the base model
- `index.ts` is the exposed entry point

By default, the revivers are only generated when needed:
- If `Date` fields are present and not stringified
- If `dictionaries` are present
- If `modelExtension` is enabled

If you open the file `libs/sdk/openapitools.json`, you can see that we have set the value of `allowModelExtension` to `true`.
This way, we make sure that the revivers will always be generated.

Now that we've seen the base model, let's start with the extension.

#### Creating the extended model
The extended model will follow a similar structure to the base model.
In the folder `libs/sdk/src/models/core/flight`, you will see the same 3 files mentioned before.

First, let's create the type `FlightCoreIfy` in `libs/sdk/src/models/core/flight.ts`.
This type should extend the type `Flight`, imported from the `base` folder and add a new field `id` of type `string`.

> [!WARNING]
> The naming convention requires the core model to contain the suffix `CoreIfy`.\
> You can find more information on core models in the
> <a href="https://github.com/AmadeusITGroup/otter/blob/main/docs/api-sdk/SDK_MODELS_HIERARCHY.md" target="_blank">SDK models hierarchy documentation</a>.

#### Creating the extended reviver
Now that you have your extended model, let's create the associated reviver in `libs/sdk/src/models/core/flight.reviver.ts`.\
This extended reviver will call the reviver of the base `Flight` model and add the `id` to the returned object.

#### Updating the exports
Once the core model and its reviver are created, we can go back to the base model to update the exported models and revivers.\
Update the file `libs/sdk/src/models/base/flight/index.ts` to export your extended model and reviver instead of the original.
sdo-1A marked this conversation as resolved.
Show resolved Hide resolved

#### Seeing the result
Your extension should now be working!\
Check out the preview to see if the `id` has been added to the model.

> [!TIP]
> Don't forget to check out the solution of this exercise!

Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { FlightCoreIfy, reviveFlightFactory } from '../../core/flight';
import type { Flight as BaseModel } from './flight';
import { reviveFlight as baseReviver } from './flight.reviver';

export type Flight = FlightCoreIfy<BaseModel>;
export const reviveFlight = reviveFlightFactory(baseReviver);
export type { BaseModel as BaseFlight };
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Flight } from '../../base/flight/flight';
import type { reviveFlight } from '../../base/flight/flight.reviver';
import type { FlightCoreIfy } from './flight';

/**
* Extended reviver for Flight
*
* @param baseRevive
*/
export function reviveFlightFactory<R extends typeof reviveFlight>(baseRevive: R) {
const reviver = <T extends Flight = Flight>(data: any, dictionaries?: any) => {
const revivedData = baseRevive<FlightCoreIfy<T>>(data, dictionaries);
if (!revivedData) { return; }
/* Set the value of your new fields here */
revivedData.id = 'sampleIdValue';
return revivedData;
fpaul-1A marked this conversation as resolved.
Show resolved Hide resolved
};

return reviver;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Flight } from '../../base/flight/flight';
import type { IgnoreEnum } from '@ama-sdk/core';

/**
* Extended type for Flight
*/
export type FlightCoreIfy<T extends IgnoreEnum<Flight>> = T & {
id: string;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './flight';
export * from './flight.reviver';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Export your core models here
export * from './flight';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './base';
export * from './core';
8 changes: 4 additions & 4 deletions packages/@ama-sdk/core/src/plugins/mock-intercept/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Mock intercept plugin

The mock interception statregy works based on two interceptions: request and fetch. For each interception, a plugin has been made.
The mock interception strategy works based on two interceptions: request and fetch. For each interception, a plugin has been made.

## Mock intercept request plugin

Expand Down Expand Up @@ -66,7 +66,7 @@ Example of usage:
*/
import {OPERATION_ADAPTER} from '@ama-sdk/sdk/spec/operation-adapter';

const myRandomAdapter: new RandomMockAdapter(
const myRandomAdapter = new RandomMockAdapter(
OPERATION_ADAPTER,
{
// Mock data for createCart operation
Expand All @@ -76,7 +76,7 @@ const myRandomAdapter: new RandomMockAdapter(
}
);

const myRandomAdapter: new SequentialMockAdapter(
const myRandomAdapter = new SequentialMockAdapter(
OPERATION_ADAPTER,
{
// Mock data for createCart operation
Expand Down Expand Up @@ -110,7 +110,7 @@ Example of usage:
*/
import {OPERATION_ADAPTER} from '@ama-sdk/sdk/spec/operation-adapter';

const myAdapter: new RandomMockAdapter(
const myAdapter = new RandomMockAdapter(
OPERATION_ADAPTER,
() => {
return fetch('http://my-test-server/getMocks');
Expand Down
18 changes: 18 additions & 0 deletions packages/@o3r-training/training-sdk/.eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,24 @@ module.exports = {
'import/resolver': 'node'
},
'overrides': [
{
'files': ['*.ts'],
'rules': {
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/naming-convention': 'off',
'@typescript-eslint/restrict-template-expressions': 'off',
'max-len': 'off',
'no-redeclare': 'off',
'no-use-before-define': 'off',
'no-useless-escape': 'off'
}
},
{
'files': ['*.jasmine.fixture.ts', '*api.fixture.ts'],
'rules': {
'jest/no-jasmine-globals': 'off'
}
},
{
'files': ['*.helper.ts'],
'rules': {
Expand Down
4 changes: 4 additions & 0 deletions packages/@o3r-training/training-sdk/open-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ paths:
responses:
200:
description: "Successful operation"
content:
application/json:
schema:
$ref: '#/components/schemas/Flight'
components:
schemas:
Flight:
Expand Down
4 changes: 2 additions & 2 deletions packages/@o3r-training/training-sdk/openapitools.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
"$schema": "https://raw.githubusercontent.com/OpenAPITools/openapi-generator-cli/master/apps/generator-cli/src/config.schema.json",
"spaces": 2,
"generator-cli": {
"version": "7.4.0",
"version": "7.9.0",
"storageDir": ".openapi-generator",
"generators": {
"ama-sdk-training-sdk": {
"generatorName": "typescriptFetch",
"output": ".",
"inputSpec": "./open-api.yml",
"inputSpec": "./open-api.yaml",
"globalProperty": {
"stringifyDate": false,
"allowModelExtension": true
Expand Down
2 changes: 1 addition & 1 deletion packages/@o3r-training/training-sdk/readme.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@


### Based on SDK spec version 1.0.0 (using openapi 3.0.2)
### Based on API specification version 1.0.0 (using openapi 3.0.2)

The SDK contains 2 different parts:

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Flight } from '../../models/base/flight/index';

import { DummyApi, DummyApiDummyGetRequestData } from './dummy-api';

Expand All @@ -6,9 +7,9 @@ export class DummyApiFixture implements Partial<Readonly<DummyApi>> {
/** @inheritDoc */
public readonly apiName = 'DummyApi';

/**
/**
* Fixture associated to function dummyGet
*/
public dummyGet: jest.Mock<Promise<void>, [DummyApiDummyGetRequestData]> = jest.fn();
public dummyGet: jest.Mock<Promise<Flight>, [DummyApiDummyGetRequestData]> = jest.fn();
}

13 changes: 7 additions & 6 deletions packages/@o3r-training/training-sdk/src/api/dummy/dummy-api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Api, ApiClient, ApiTypes, computePiiParameterTokens, RequestBody, RequestMetadata, } from '@ama-sdk/core';
import { Flight, reviveFlight } from '../../models/base/flight/index';
import { Api, ApiClient, ApiTypes, computePiiParameterTokens, RequestBody, RequestMetadata } from '@ama-sdk/core';

/** Parameters object to DummyApi's dummyGet function */
export interface DummyApiDummyGetRequestData {
Expand Down Expand Up @@ -27,20 +28,20 @@ export class DummyApi implements Api {
}

/**
*
*
*
*
* @param data Data to provide to the API call
* @param metadata Metadata to pass to the API call
*/
public async dummyGet(data: DummyApiDummyGetRequestData, metadata?: RequestMetadata<string, string>): Promise<void> {
public async dummyGet(data: DummyApiDummyGetRequestData, metadata?: RequestMetadata<string, 'application/json'>): Promise<Flight> {
const queryParams = this.client.extractQueryParams<DummyApiDummyGetRequestData>(data, [] as never[]);
const metadataHeaderAccept = metadata?.headerAccept || 'application/json';
const headers: { [key: string]: string | undefined } = {
'Content-Type': metadata?.headerContentType || 'application/json',
...(metadataHeaderAccept ? {'Accept': metadataHeaderAccept} : {})
};

let body: RequestBody = '';
const body: RequestBody = '';
const basePath = `${this.client.options.basePath}/dummy`;
const tokenizedUrl = `${this.client.options.basePath}/dummy`;
const tokenizedOptions = this.client.tokenizeRequestOptions(tokenizedUrl, queryParams, this.piiParamTokens, data);
Expand All @@ -59,7 +60,7 @@ export class DummyApi implements Api {
const options = await this.client.getRequestOptions(requestOptions);
const url = this.client.prepareUrl(options.basePath, options.queryParams);

const ret = this.client.processCall<void>(url, options, ApiTypes.DEFAULT, DummyApi.apiName, { 200: undefined } , 'dummyGet');
const ret = this.client.processCall<Flight>(url, options, ApiTypes.DEFAULT, DummyApi.apiName, { 200: reviveFlight } , 'dummyGet');
return ret;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export {};


/** Validation regex for a API field name */
export const dapiFieldNamePattern = /^[a-z][a-zA-Z0-9\[\]_]*$/;
Loading