Skip to content

Commit 1582900

Browse files
committed
[IMP] doc: connecting to external API
Connecting to an external API is probably the number 1 question we have. This commit improves (and rename) the "async function" documentation, using a more step-by-step approach. It's also added in the landing page to improve its discoverability, since it's a common question. Hopefully, this version is better than the previous one Some of it was rephrased by ChatGPT. closes #2560 Signed-off-by: Rémi Rahir (rar) <rar@odoo.com>
1 parent 7c81c88 commit 1582900

File tree

2 files changed

+125
-48
lines changed

2 files changed

+125
-48
lines changed

doc/add_function.md

Lines changed: 117 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
- [Processing all values of a specific reference argument](#processing-all-values-of-a-specific-reference-argument)
1313
- [Processing all values of all arguments at once](#processing-all-values-of-all-arguments-at-once)
1414
- [Custom external dependency](#custom-external-dependency)
15-
- [Asynchronous function](#asynchronous-function)
15+
- [Connecting to an external API](#connecting-to-an-external-api)
1616

1717
## Adding a new custom function
1818

@@ -420,33 +420,34 @@ addFunction("USER.NAME", {
420420
});
421421
```
422422
423-
## Asynchronous function
423+
## Connecting to an external API
424424
425-
Function are synchronous. However, you can use a `getter` to fetch data from an [external source](#external-dependency).
426-
Here is what a function fetching currency rate from a server might look like.
425+
This section provides a step-by-step guide on implementing a function that connects to an external API in the o-spreadsheet library.
426+
427+
To illustrate the process, let's create a simple function called `CURRENCY.RATE` that returns the exchange rate between two currencies, such as `USD` and `EUR`.
428+
429+
Here is the basic structure of our `CURRENCY.RATE` function:
427430
428431
```ts
429432
addFunction("CURRENCY.RATE", {
430433
description:
431-
"This function takes in two currency codes as arguments, and returns the exchange rate from the first currency to the second as float.",
432-
compute: function (currencyFrom, currencyTo) {
433-
const from = toString(currencyFrom);
434-
const to = toString(currencyTo);
435-
const currencyRate = this.getters.getCurrencyRate(from, to);
436-
if (currencyRate.status === "LOADING") {
437-
throw new Error("Loading...");
438-
}
439-
return currencyRate.value;
440-
},
434+
"This function takes two currency codes as input and returns the exchange rate from the first currency to the second as a floating-point number.",
441435
args: [
442-
arg("currency_from (string)", "First currency code."),
443-
arg("currency_to (string)", "Second currency code."),
436+
arg("currency_from (string)", "The code of the first currency."),
437+
arg("currency_to (string)", "The code of the second currency."),
444438
],
439+
compute: function (currencyFrom, currencyTo) {
440+
// TODO: Implement the function logic here
441+
},
445442
returns: ["NUMBER"],
446443
});
447444
```
448445
449-
And add a [plugin](./extending/architecture.md#plugins) to handle data loading.
446+
The `compute` function inside the function definition can use external dependencies available in its evaluation context. Refer to the [Custom external dependency](#custom-external-dependency) section for more details on how to implement data fetching and caching in your preferred manner.
447+
448+
To adhere to the o-spreadsheet's architecture, we'll use a dedicated [plugin](./extending/architecture.md#plugins) for this purpose. The `compute` function can access relevant data using its getters.
449+
450+
First, let's create the `CurrencyPlugin` class that extends `UIPlugin` and registers the necessary getters:
450451
451452
```ts
452453
const { uiPluginRegistry } = o_spreadsheet.registries;
@@ -457,45 +458,120 @@ class CurrencyPlugin extends UIPlugin {
457458

458459
constructor(config) {
459460
super(config);
461+
}
460462

461-
/**
462-
* You can add whatever you need to the `config.custom` property at the model
463-
* creation
464-
*/
465-
this.server = config.custom.server;
463+
getCurrencyRate(from: string, to: string) {
464+
// TODO: Implement the logic to retrieve the currency rate
465+
}
466+
}
467+
468+
uiPluginRegistry.add("currencyPlugin", CurrencyPlugin);
469+
```
470+
471+
Next, we need to update the `compute` function to use the `getCurrencyRate` getter:
472+
473+
```ts
474+
addFunction("CURRENCY.RATE", {
475+
// ...
476+
compute: function (currencyFrom, currencyTo) {
477+
const from = toString(currencyFrom);
478+
const to = toString(currencyTo);
479+
return this.getters.getCurrencyRate(from, to);
480+
},
481+
// ...
482+
});
483+
```
466484
467-
// a cache to store fetched rates
468-
this.currencyRates = {};
485+
Now, let's address an issue: **spreadsheet functions are synchronous**. This means that our getter `getCurrencyRate` also needs to return synchronously.
486+
487+
To handle this requirement and enable caching of API results, we'll introduce a simple `cache` data structure within our plugin. Caching is important to avoid making repeated API calls when the function is evaluated multiple times during spreadsheet editing.
488+
489+
The `getCurrencyRate` function reads from the cache and returns the status. If the status is `"missing"`, the `fetch` method handles data fetching and updates the cache. The `getFromCache` and `fetch` methods are described below:
490+
491+
```ts
492+
class CurrencyPlugin extends UIPlugin {
493+
static getters = ["getCurrencyRate"];
494+
495+
constructor(config) {
496+
super(config);
497+
this.cache = {};
469498
}
470499

471500
getCurrencyRate(from: string, to: string) {
472-
const value = this.getFromCache(from, to);
473-
if (value !== undefined) {
474-
return value;
501+
const rate = this.getFromCache(from, to);
502+
switch (rate.status) {
503+
case "missing":
504+
this.fetch(from, to);
505+
throw new Error("Loading...");
506+
case "pending":
507+
throw new Error("Loading...");
508+
case "fulfilled":
509+
return rate.value;
510+
case "rejected":
511+
throw rate.error;
512+
default:
513+
throw new Error("An unexpected error occurred");
475514
}
476-
// start fetching the data
477-
this.server.fetchCurrencyRate(from, to).then((result) => {
478-
this.setCacheValue(from, to, result);
479-
// don't forget to trigger a new evaluation when the data is loaded!
480-
this.dispatch("EVALUATE_CELLS");
481-
});
482-
// return synchronously
483-
return { status: "LOADING" };
484515
}
516+
}
517+
```
518+
519+
Let's explore a possible implementation of the `getFromCache` and `fetch` methods:
520+
521+
```ts
522+
class CurrencyPlugin extends UIPlugin {
523+
// ...
485524

486525
private getFromCache(from: string, to: string) {
487526
const cacheKey = `${from}-${to}`;
488-
if (cacheKey in this.currencyRates) {
489-
return this.currencyRates[cacheKey];
527+
if (cacheKey in this.cache) {
528+
return this.cache[cacheKey];
490529
}
491-
return undefined;
530+
return { status: "missing" };
492531
}
493532

494-
private setCacheValue(from: string, to: string, value: number) {
533+
private fetch(from: string, to: string) {
495534
const cacheKey = `${from}-${to}`;
496-
this.currencyRates[cacheKey] = { value: result, status: "COMPLETED" };
535+
// Mark the value as "pending" in the cache
536+
this.cache[cacheKey] = { status: "pending" };
537+
538+
// Assume we have an endpoint `https://api.example.com/rate/<from>/<to>` to fetch the currency rate.
539+
fetch(`https://api.example.com/rate/${from}/${to}`)
540+
.then((response) => response.json())
541+
.then((data) => {
542+
// Update the cache with the result
543+
this.cache[cacheKey] = {
544+
status: "fulfilled",
545+
result: data.rate,
546+
};
547+
})
548+
.catch((error) => {
549+
// Update the cache with the error
550+
this.cache[cacheKey] = {
551+
status: "rejected",
552+
error,
553+
};
554+
})
555+
.finally(() => {
556+
// Trigger a new evaluation when the data is loaded
557+
this.dispatch("EVALUATE_CELLS");
558+
});
497559
}
498560
}
561+
```
499562
500-
uiPluginRegistry.add("currencyPlugin", CurrencyPlugin);
563+
Instead of using the native `fetch` method, you can inject your own service through the configuration:
564+
565+
```ts
566+
class CurrencyPlugin extends UIPlugin {
567+
constructor(config) {
568+
super(config);
569+
/**
570+
* You can add whatever you need to the `config.custom` property during model creation
571+
*/
572+
this.rateAPI = config.custom.rateAPI;
573+
}
574+
}
501575
```
576+
577+
By following these steps, you can successfully connect to an external API and implement custom functions in the o-spreadsheet library.

readme.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,14 @@ a.k.a. "[Owly](https://github.com/odoo/owl) Sheet" 🦉
3232

3333
1. [Architecture](doc/extending/architecture.md)
3434
2. [Custom function](doc/add_function.md)
35-
3. [Business feature](doc/extending/business_feature.md)
36-
4. Menu items (under construction)
37-
5. Side panel (under construction)
38-
6. Notification (under construction)
39-
7. Export Excel (under construction)
40-
8. [Terminology](doc/o-spreadsheet_terminology.png)
41-
9. [API](doc/tsdoc/README.md)
35+
3. [Connecting to an external API](doc/add_function.md#connecting-to-an-external-api)
36+
4. [Business feature](doc/extending/business_feature.md)
37+
5. Menu items (under construction)
38+
6. Side panel (under construction)
39+
7. Notification (under construction)
40+
8. Export Excel (under construction)
41+
9. [Terminology](doc/o-spreadsheet_terminology.png)
42+
10. [API](doc/tsdoc/README.md)
4243

4344
## Run it!
4445

0 commit comments

Comments
 (0)