Skip to content

Commit e0e2ad6

Browse files
committed
Add section on API modelling
1 parent 17e3b0a commit e0e2ad6

File tree

6 files changed

+168
-9
lines changed

6 files changed

+168
-9
lines changed
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
---
2+
title: API Modelling
3+
description: Learn more about the API modelling process of @rescript/webapi.
4+
slug: "04-api-modelling"
5+
---
6+
7+
import { Aside, Code, Icon } from "@astrojs/starlight/components";
8+
9+
One of this projects goals is to provide a consistent and idiomatic API for the Web APIs.
10+
The interopt story of ReScript is quite good, but it it has limitations.
11+
JavaScript is a dynamic language and has a lot of flexibility.
12+
ReScript is a statically typed language and has to model the dynamic parts of JavaScript in a static way.
13+
14+
## Dynamic parameters and properties
15+
16+
Some Web APIs have a parameter or property that can be multiple things.
17+
In ReScript, you would model this as a variant type. This is an example wrapper around values with a key property to discriminate them.
18+
19+
In JavaScript, this strictness is not enforced and you can pass a string where a number is expected.
20+
There are multiple strategies to model this in ReScript and it depends on the specific API which one is the best.
21+
22+
### Overloads
23+
24+
One example is [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener).
25+
This can access either a boolean or an object as the third parameter.
26+
27+
```js
28+
addEventListener(type, listener);
29+
addEventListener(type, listener, options);
30+
addEventListener(type, listener, useCapture);
31+
```
32+
33+
Because, this is a method, we can model this as an overloaded function in ReScript.
34+
The first two overloads are the same, so we can merge them into one with an optional options parameter.
35+
36+
```ReScript
37+
@send
38+
external addEventListener: (
39+
htmlButtonElement,
40+
eventType,
41+
eventListener<'event>,
42+
~options: addEventListenerOptions=?,
43+
) => unit = "addEventListener"
44+
```
45+
46+
The third overload takes a boolean and is worth using when you want to change the default of the `useCapture` boolean parameter.
47+
We can use [a fixed argument](https://rescript-lang.org/docs/manual/latest/bind-to-js-function#fixed-arguments) to model this.
48+
49+
```ReScript
50+
@send
51+
external addEventListener_useCapture: (
52+
htmlButtonElement,
53+
~type_: eventType,
54+
~callback: eventListener<'event>,
55+
@as(json`true`) _,
56+
) => unit = "addEventListener"
57+
```
58+
59+
<Aside type="caution" title="Improving methods">
60+
Be aware that inherited interfaces duplicate their inherited methods. This
61+
means that improving `addEventListener` in the `EventTarget` interface
62+
requires to update all interfaces that inherit from `EventTarget`.
63+
</Aside>
64+
65+
### Decoded variants
66+
67+
We can be pragmatic with overloaded functions and use model them in various creative ways.
68+
For properties, we **cannot do this unfortunately**. A propery can only be defined once and have a single type.
69+
70+
The strategy here is to use a decoded variant.
71+
72+
Example for the [fillStyle](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/fillStyle) property of the `CanvasRenderingContext2D` interface can be either a:
73+
74+
- `string`
75+
- `CanvasGradient`
76+
- `CanvasPattern`
77+
78+
These types are not all primitives and thus we cannot define it as [untagged variants](https://rescript-lang.org/docs/manual/latest/variant#untagged-variants).
79+
What we can do instead is represent the type as an empty type and use a helper module to interact with this.
80+
81+
export const fillStyleDef = `
82+
type fillStyle
83+
84+
type canvasRenderingContext2D = {
85+
// ... other propeties
86+
mutable fillStyle: fillStyle
87+
}
88+
`;
89+
90+
<Code code={fillStyleDef} title="DOMAPI.res" lang="ReScript"></Code>
91+
92+
When we wish to read and write the `fillStyle` property, we can use a helper module to lift the type to an actual ReScript variant:
93+
94+
export const fillStyleModule = `
95+
open Prelude
96+
open CanvasAPI
97+
open DOMAPI
98+
99+
external fromString: string => fillStyle = "%identity"
100+
external fromCanvasGradient: canvasGradient => fillStyle = "%identity"
101+
external fromCanvasPattern: canvasGradient => fillStyle = "%identity"
102+
103+
type decoded =
104+
| String(string)
105+
| CanvasGradient(canvasGradient)
106+
| CanvasPattern(canvasPattern)
107+
108+
let decode = (t: fillStyle): decoded => {
109+
if CanvasGradient.isInstanceOf(t) {
110+
CanvasGradient(unsafeConversation(t))
111+
} else if CanvasPattern.isInstanceOf(t) {
112+
CanvasPattern(unsafeConversation(t))
113+
} else {
114+
String(unsafeConversation(t))
115+
}
116+
}
117+
`
118+
119+
<Code
120+
code={fillStyleModule}
121+
title="DOMAPI/FillStyle.res"
122+
lang="ReScript"
123+
></Code>
124+
125+
We can now use `FillStyle.decode` to get the actual value of the `fillStyle` property.
126+
And use `FillStyle.fromString`, `FillStyle.fromCanvasGradient`, and `FillStyle.fromCanvasPattern` to set the value.
127+
128+
```ReScript
129+
let ctx = myCanvas->HTMLCanvasElement.getContext_2D
130+
131+
// Write
132+
ctx.fillStyle = FillStyle.fromString("red")
133+
134+
// Read
135+
switch ctx.fillStyle->FillStyle.decode {
136+
| FillStyle.String(color) => Console.log(`Color: ${color}`)
137+
| FillStyle.CanvasGradient(_) => Console.log("CanvasGradient")
138+
| FillStyle.CanvasPattern(_) => Console.log("CanvasPattern")
139+
}
140+
```
141+
142+
<Icon
143+
name="information"
144+
color="var(--sl-color-text-accent)"
145+
class="inline-icon"
146+
size="1.5rem"
147+
/>
148+
Try and use `decoded` and `decode` as conventions for the type and function
149+
names.

docs/content/docs/contributing/code-generation.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ external fetch: (window, string, ~init: requestInit=?)
4343
4444
/** TODO: add better docs */
4545
@send
46-
external fetch_with_request: (window, request, ~init: requestInit=?)
46+
external fetch_withRequest: (window, request, ~init: requestInit=?)
4747
=> Promise.t<response> = "fetch"
4848
```
4949

docs/content/docs/contributing/documentation.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: "Documentation"
33
description: Learn more about the relevance of adding documentation to @rescript/webapi.
4-
slug: "05-documentation"
4+
slug: "06-documentation"
55
---
66

77
After the bindings are generated, all you got was a link to the MDN documentation.

docs/content/docs/contributing/module-structure.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ The bindings are organized by the Web API they represent. Each API has its inter
1414
- package.json
1515
- src
1616
- DOMAPI.res
17-
- DOMAPI/
17+
- DOMAPI
1818
- HTMLElement.res
1919

2020
</FileTree>

docs/content/docs/contributing/testing.mdx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
---
22
title: Testing
33
description: Learn more about testing the bindings for @rescript/webapi.
4-
slug: "04-testing"
4+
slug: "05-testing"
55
---
66

7-
import { Aside } from "@astrojs/starlight/components";
7+
import { Aside, FileTree } from "@astrojs/starlight/components";
88

99
Adding some unit tests is a great way to ensure that the bindings work as expected.
1010
It illustrates how the bindings are supposed to be used and can help to catch regressions.
@@ -13,11 +13,16 @@ It illustrates how the bindings are supposed to be used and can help to catch re
1313

1414
Create a new help in the `test` folder with the same name as the module you want to test, followed by `_test.res`.
1515

16-
import { FileTree } from "@astrojs/starlight/components";
17-
1816
<FileTree>
19-
- src - DOMAPI.res - DOMAPI/ - HTMLCanvasElement.res - test - DOMAPI/ -
20-
HTMLCanvasElement_test.res
17+
18+
- src
19+
- DOMAPI.res
20+
- DOMAPI
21+
- HTMLCanvasElement.res
22+
- tests
23+
- DOMAPI
24+
- HTMLCanvasElement_test.res
25+
2126
</FileTree>
2227

2328
Add a small sample of valid code that uses the bindings you've changed.

docs/styles/theme.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,8 @@ body {
4343
background-color: var(--scrollbar-thumb-background);
4444
}
4545
}
46+
47+
.sl-markdown-content .inline-icon {
48+
display: inline-block;
49+
vertical-align: text-bottom;
50+
}

0 commit comments

Comments
 (0)