Skip to content

Commit

Permalink
feat: otter sdk training - model extension
Browse files Browse the repository at this point in the history
  • Loading branch information
sdo-1A authored and fpaul-1A committed Nov 18, 2024
1 parent 701f1d8 commit f929022
Show file tree
Hide file tree
Showing 26 changed files with 824 additions and 20 deletions.
134 changes: 134 additions & 0 deletions apps/showcase/src/assets/trainings/sdk/program.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
[
{
"title": "Welcome to the Otter SDK tutorial",
"htmlContentUrl": "./steps/welcome/instructions.md"
},
{
"title": "How to use the Otter SDK?",
"htmlContentUrl": "./steps/typescript-sdk/instructions.md"
},
{
"title": "Customize your fetch client with plugins",
"htmlContentUrl": "./steps/plugins/instructions.md"
},
{
"title": "Integrate your SDK in Angular",
"htmlContentUrl": "./steps/angular-integration/instructions.md",
"filesConfiguration": {
"name": "angular-integration",
"startingFile": "apps/tutorial-app/src/app/app.component.ts",
"urls": [
{
"path": ".",
"contentUrl": "./shared/monorepo-template.json"
},
{
"path": "./libs/sdk/src",
"contentUrl": "@o3r-training/training-sdk/structure/src.json"
}
],
"mode": "interactive",
"commands": ["npm ci --legacy-peer-deps --ignore-scripts --no-audit", "npm run ng run sdk:build", "npm run ng run tutorial-app:serve"]
}
},
{
"title": "Generate your first SDK - Specifications",
"htmlContentUrl": "./steps/sdk-specs/instructions.md"
},
{
"title": "Generate your first SDK - Command",
"htmlContentUrl": "./steps/sdk-generation/instructions.md"
},
{
"title": "SDK with Dates - Generation",
"htmlContentUrl": "./steps/date-sdk-generation/instructions.md",
"filesConfiguration": {
"name": "generate-date-sdk",
"startingFile": "open-api.yaml",
"urls": [
{
"path": ".",
"contentUrl": "./steps/date-sdk-generation/exercise.json"
}
],
"solutionUrls": [
{
"path": ".",
"contentUrl": "@o3r-training/training-sdk/structure/spec.json"
},
{
"path": "./src",
"contentUrl": "@o3r-training/training-sdk/structure/src.json"
}
],
"mode": "readonly",
"commands": []
}
},
{
"title": "SDK with Dates - How to use",
"htmlContentUrl": "./steps/date/instructions.md",
"filesConfiguration": {
"name": "utils-date",
"startingFile": "apps/app/src/app/app.component.ts",
"urls": [
{
"path": ".",
"contentUrl": "./shared/monorepo-with-app.json"
},
{
"path": "./apps/app/src/app",
"contentUrl": "./steps/date/exercise.json"
}
],
"solutionUrls": [
{
"path": "./apps/app/src/app",
"contentUrl": "./steps/date/solution.json"
}
],
"mode": "interactive",
"commands": [
"npm ci --legacy-peer-deps --ignore-scripts --no-audit",
"npm run ng run app:serve"
]
}
},
{
"title": "SDK with model extension",
"htmlContentUrl": "./steps/model-extension/instructions.md",
"filesConfiguration": {
"name": "model-extension",
"startingFile": "apps/tutorial-app/src/app/app.config.ts",
"urls": [
{
"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"
},
{
"path": "./libs/sdk/src/models",
"contentUrl": "./steps/model-extension/exercise.json"
}
],
"solutionUrls": [
{
"path": "./libs/sdk/src/models",
"contentUrl": "./steps/model-extension/solution.json"
}
],
"mode": "interactive",
"commands": [
"npm ci --legacy-peer-deps --ignore-scripts --no-audit",
"npm run ng run tutorial-app:serve"
]
}
}
]
419 changes: 419 additions & 0 deletions apps/showcase/src/assets/trainings/sdk/shared/monorepo-template.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"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 */
/* Add an extra field `id: string` */

/**
* 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,56 @@
<p>
There are certain cases in which you want to be able to extend your SDK models, and therefore ensure that revivers are generated.
</p>
<p>
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 can create a new model which extends the previously generated <code>Flight</code> type.
</p>
<p>
Before beginning the exercise, we need to make sure that the existing API was generated with model extension in the previous steps.
To verify this, check out the configuration used to generate the API (either the command line or the file <code>openapitools.json</code>).
Ensure that the global property <code>allowModelExtension</code> has been set to <b>true</b>. This will guarantee that the revivers of the
base models are generated, which is essential for the following exercise.
</p>

<h3>Exercise</h3>
<p>
Extensions of base models (located in the <b>base</b> folder) are created in the <b>core</b> folder.
</p>
<p>
First, create the type <code>FlightCoreIfy</code> which extends <code>Flight</code>, imported from the <b>base</b> folder.
You can do this using the template file <code>flight.ts</code>.
</p>
<p>
<i>
Note: The naming convention requires the core model to contain the suffix <code>CoreIfy</code>. 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>.
</i>
</p>
<p>
Then, you can create the reviver for this new core model using the existing template file <code>flight.reviver.ts</code>. This reviver will extend the
reviver of the base <code>Flight</code> model, which should exist since we have ensured this during the prerequisites of the exercise.
</p>
<p>
Once the core model and its reviver are created, we can go back to the base model to update the exported models and revivers.
You can do so in the file <code>base/flight/index.ts</code> by following this template:
</p>
<pre class="w-100 bg-body-tertiary px-5 pre-whitespace">
<code>
// in the models/base/my-model/index.ts
import { MyModelCoreIfy, reviveMyModelFactory } from '../../core/my-model';
import type { MyModel as BaseModel } from './my-model';
import { reviveMyModel as baseReviver } from './my-model.reviver';

export type MyModel = MyModelCoreIfy<BaseModel>;
export const reviveMyModel = reviveMyModelFactory(baseReviver);
export type { MyModel as BaseMyModel };
</code>
</pre>
<p>
<i>
<u>Hint:</u> <code>MyModel</code> should be replaced by <code>Flight</code> throughout these template files.
</i>
</p>
<p>
Don't forget to check out the solution of this exercise!
</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
There are certain cases in which you want to be able to extend your SDK models, and therefore ensure that revivers are generated.

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 can create a new model which extends the previously generated `Flight` type.

> [!NOTE]
> For this exercise, we provide an SDK that has been generated with model extension.
>
> To verify this, check out the configuration used to generate the API (either the command line or the file `openapitools.json`).\
> Ensure that the global property `allowModelExtension` has been set to `true`.\
> This will guarantee that the revivers of the base models are generated, which is essential for the following exercise.
### Exercise

Extensions of base models (located in the `base` folder) are created in the `core` folder.

#### Creating the extended model
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: 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
Then, you can create the reviver for this new core model 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.\
The base reviver should exist since we have ensured this during the prerequisites of the exercise.

#### 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.

You should now have your extension 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;
};

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
Loading

0 comments on commit f929022

Please sign in to comment.