Skip to content

Commit

Permalink
feat: otter sdk training - integration in angular
Browse files Browse the repository at this point in the history
  • Loading branch information
sdo-1A committed Dec 4, 2024
1 parent 8c140e9 commit a641eab
Show file tree
Hide file tree
Showing 9 changed files with 332 additions and 33 deletions.
19 changes: 16 additions & 3 deletions apps/showcase/src/assets/trainings/sdk/program.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,21 +84,34 @@
"htmlContentUrl": "./steps/angular-integration/instructions.md",
"filesConfiguration": {
"name": "angular-integration",
"startingFile": "apps/tutorial-app/src/app/app.component.ts",
"startingFile": "apps/tutorial-app/src/app/app.config.ts",
"urls": [
{
"path": ".",
"contentUrl": "./shared/monorepo-template.json"
},
{
"path": "./libs/sdk",
"contentUrl": "@o3r-training/showcase-sdk/structure/spec.json"
},
{
"path": "./libs/sdk/src",
"contentUrl": "@o3r-training/training-sdk/structure/src.json"
"contentUrl": "@o3r-training/showcase-sdk/structure/src.json"
},
{
"path": "./apps/tutorial-app/src/app",
"contentUrl": "./steps/angular-integration/exercise.json"
}
],
"solutionUrls": [
{
"path": "./apps/tutorial-app/src/app",
"contentUrl": "./steps/angular-integration/solution.json"
}
],
"mode": "interactive",
"commands": [
"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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<div class="row m-2">
<div class="col">
<button class="btn btn-outline-primary justify-content-center" (click)="getAvailablePets()">Get Available Pets</button>
<table class="table">
<caption align="top">
Pets with the status <b>available</b> from the Swagger Petstore
</caption>
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Name</th>
<th scope="col">Status</th>
</tr>
</thead>
<tbody>
@for (pet of pets(); track pet.id; let index = $index) {
<tr>
<td>{{ index + 1 }}</td>
<td>{{ pet.name }}</td>
<td>{{ pet.status }}</td>
</tr>
}
</tbody>
</table>
</div>
<div class="col">
<button class="btn btn-outline-primary" (click)="getPetInventory()">Get Inventory</button>
<div>{{petsInventory() | json}}</div>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { JsonPipe } from '@angular/common';
import { Component, inject, signal } from '@angular/core';
import { ApiFactoryService } from '@o3r/apis-manager';
import { type Pet, PetApi, StoreApi } from 'sdk';

@Component({
selector: 'app-root',
standalone: true,
imports: [JsonPipe],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
/** Title of the application */
public title = 'tutorial-app';

// TODO Inject the ApiFactoryService and get the corresponding APIs
private readonly petApi = inject(PetApi);
private readonly storeApi = inject(StoreApi);

private readonly petsWritable = signal<Pet[]>([]);
public readonly pets = this.petsWritable.asReadonly();

private readonly petsInventoryWritable = signal<{ [key: string]: number }>({});
public readonly petsInventory = this.petsInventoryWritable.asReadonly();

/** Get the pets whose status is 'available' */
public async getAvailablePets() {
const availablePets = await this.petApi.findPetsByStatus({status: 'available'});
this.petsWritable.set(availablePets);
}

/** Get the pets inventory */
public async getPetInventory() {
const inventory = await this.storeApi.getInventory({});
this.petsInventoryWritable.set(inventory);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { ApiFetchClient } from '@ama-sdk/client-fetch';
import { ApiClient, PluginRunner, RequestOptions, RequestPlugin } from '@ama-sdk/core';
import { ApplicationConfig, importProvidersFrom, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { ApiManager, ApiManagerModule } from '@o3r/apis-manager';
import { PetApi, StoreApi } from 'sdk';
import { routes } from './app.routes';

class MockInterceptRequest implements RequestPlugin {
public load(): PluginRunner<RequestOptions, RequestOptions> {
return {
transform: async (data: RequestOptions) => {
const mockData = data.api?.apiName === 'PetApi'
? [{ name : "mockPetName", photoUrls: ["mockPhotoUrl"], status: "available"}]
: { mockPropertyName : "mockPropertyValue"};
const text = JSON.stringify(mockData);
const blob = new Blob([text], { type: 'application/json' });
const basePath = URL.createObjectURL(blob);
return {
method: 'GET',
basePath,
headers: new Headers()
};
}
};
}
}

class RequestAlertPlugin implements RequestPlugin {
public load(): PluginRunner<RequestOptions, RequestOptions> {
return {
transform: (data: RequestOptions) => {
alert(JSON.stringify(data));
return data;
}
};
}
}

function petApiFactory() {
const apiFetchClient = new ApiFetchClient(
{
basePath: 'https://petstore3.swagger.io/api/v3',
requestPlugins: [new MockInterceptRequest()],
replyPlugins: [],
fetchPlugins: []
}
);
return new PetApi(apiFetchClient);
}

function storeApiFactory() {
const apiFetchClient = new ApiFetchClient(
{
basePath: 'https://petstore3.swagger.io/api/v3',
requestPlugins: [new MockInterceptRequest()],
replyPlugins: [],
fetchPlugins: []
}
);
return new StoreApi(apiFetchClient);
}

// TODO Initialize apiConfig with ApiFetchClient

// TODO Add the configuration override for a specific API

export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
// TODO Replace the api factory providers with the ApiManagerModule
{provide: PetApi, useFactory: petApiFactory},
{provide: StoreApi, useFactory: storeApiFactory}
]
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
When dealing with an Angular project, you need to ensure that your `ApiClient` will be shared across your application.
The Otter framework provides the `ApiManager` service to manage your API collection.

### Objective
- Leverage the `ApiManager` service to access two different clients to retrieve the list of available pets and get the Swagger Petstore inventory.
- Add a plugin to the `StoreApi` to alert each time a call is sent.

### Prerequisite
- The package `@o3r/apis-manager` needs to be installed (which has already been done for you).

### Exercise

#### Existing plugins
As you can see in the `app.config.ts` file, a plugin `RequestAlertPlugin` has been created which displays an alert box when the API receives a request.
There is also a `MockInterceptRequest` plugin, similar to the one created in the previous step, to mock the request plugin.

#### Integrate the ApiManagerModule with default configuration
First, create the variable `apiConfig` with the properties of `ApiFetchClient`. This default configuration should contain the `MockInterceptRequest`
plugin (similar to the exercise in the previous step). Here is a template to get you started:
```typescript
// Default configuration for all the APIs defined in the ApiManager
const apiConfig: ApiClient = new ApiFetchClient(
{
// Properties of ApiFetchClient
}
);
```

Next, create the `apiManager` variable like this:
```typescript
const apiManager = new ApiManager(apiConfig);
```

You can now integrate the `ApiManagerModule` in the providers of your `ApplicationConfig`. You can use the following lines to guide you:
```typescript
export const appConfig: ApplicationConfig = {
providers: [importProvidersFrom(ApiManagerModule.forRoot(apiManager))]
};
```

> [!NOTE]
> This integration should replace the previous providers of `PetApi` and `StoreApi` using the factories `petApiFactory` and `storeApiFactory`
> in the `ApplicationConfig`.
Then, checkout the `app.component.ts` file and update the variables `petApi` and `storeApi` by injecting the `ApiFactoryService` to use your
unique instance of the `StoreApi` and `PetApi`.

Now, when clicking the **Get Available Pets** button, your table should be updated with the mock value of available pets and when clicking the
**Get Inventory** button, you should see the mock value of inventory.

#### Override of the default configuration
Let's override the default configuration by updating `apiManager` and configuring it to use the `RequestAlertPlugin` in the `StoreApi`.
You can inspire yourself with the following lines:

```typescript
const apiManager = new ApiManager(apiConfig, {
// Configuration override for a specific API
StoreApi: new ApiFetchClient({
// Properties of ApiFetchClient
})
});
```

Let's see how this new configuration override impacts the default configuration.
When clicking the **Get Available Pets** button, your table should still display the mock value of available pets (without the alert box).
When clicking the **Get Inventory** button, you should see the request to the `StoreApi` logged in the alert box and the actual result displayed
in the UI (not the mock value like before).

We can conclude that the configuration override does not merge the plugins, but replaces them.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { JsonPipe } from '@angular/common';
import { Component, inject, signal } from '@angular/core';
import { ApiFactoryService } from '@o3r/apis-manager';
import { type Pet, PetApi, StoreApi } from 'sdk';

@Component({
selector: 'app-root',
standalone: true,
imports: [JsonPipe],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
/** Title of the application */
public title = 'tutorial-app';

private readonly petApi = inject(ApiFactoryService).getApi(PetApi);
private readonly storeApi = inject(ApiFactoryService).getApi(StoreApi);

private readonly petsWritable = signal<Pet[]>([]);
public readonly pets = this.petsWritable.asReadonly();

private readonly petsInventoryWritable = signal<{ [key: string]: number }>({});
public readonly petsInventory = this.petsInventoryWritable.asReadonly();

/** Get the pets whose status is 'available' */
public async getAvailablePets() {
const availablePets = await this.petApi.findPetsByStatus({status: 'available'});
this.petsWritable.set(availablePets);
}

/** Get the pets inventory */
public async getPetInventory() {
const inventory = await this.storeApi.getInventory({});
this.petsInventoryWritable.set(inventory);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { ApiFetchClient } from '@ama-sdk/client-fetch';
import { ApiClient, PluginRunner, RequestOptions, RequestPlugin } from '@ama-sdk/core';
import { ApplicationConfig, importProvidersFrom, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { ApiManager, ApiManagerModule } from '@o3r/apis-manager';
import { routes } from './app.routes';

class MockInterceptRequest implements RequestPlugin {
public load(): PluginRunner<RequestOptions, RequestOptions> {
return {
transform: async (data: RequestOptions) => {
const mockData = data.api?.apiName === 'PetApi'
? [{ name : "mockPetName", photoUrls: ["mockPhotoUrl"], status: "available"}]
: { mockPropertyName : "mockPropertyValue"};
const text = JSON.stringify(mockData);
const blob = new Blob([text], { type: 'application/json' });
const basePath = URL.createObjectURL(blob);
return {
method: 'GET',
basePath,
headers: new Headers()
};
}
};
}
}

class RequestAlertPlugin implements RequestPlugin {
public load(): PluginRunner<RequestOptions, RequestOptions> {
return {
transform: (data: RequestOptions) => {
alert(JSON.stringify(data));
return data;
}
};
}
}

// Default configuration for all the APIs defined in the ApiManager
const apiConfig: ApiClient = new ApiFetchClient(
{
basePath: 'https://petstore3.swagger.io/api/v3',
requestPlugins: [new MockInterceptRequest()],
fetchPlugins: []
}
);

const apiManager = new ApiManager(apiConfig, {
// Configuration override for a specific API
StoreApi: new ApiFetchClient({
basePath: 'https://petstore3.swagger.io/api/v3',
requestPlugins: [new RequestAlertPlugin()],
fetchPlugins: []
})
});

export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
importProvidersFrom(ApiManagerModule.forRoot(apiManager))
]
};
27 changes: 0 additions & 27 deletions tools/github-actions/new-version/packaged-action/LICENSE.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit a641eab

Please sign in to comment.