Skip to content

Commit

Permalink
v0.3.0 🚀 - Support path params
Browse files Browse the repository at this point in the history
  • Loading branch information
DZakh committed Jun 20, 2024
1 parent 62df2f2 commit 1450277
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 12 deletions.
37 changes: 33 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ Add `rescript-rest` to `bs-dependencies` in your `rescript.json`:
Easily define your API contract somewhere shared, for example, `Contract.res`:

```rescript
let createPost = Rest.route(() => {
path: "/posts",
method: "POST",
variables: s => {
"title": s.field("title", S.string),
"body": s.field("body", S.string),
},
})
let getPost = Rest.route(() => {
path: "/posts/:id",
method: "GET",
variables: s => s.param("id", S.string),
})
let getPosts = Rest.route(() => {
path: "/posts",
method: "GET",
Expand All @@ -56,6 +71,22 @@ Consume the api on the client with a RPC-like interface:
```rescript
let client = Rest.client(~baseUrl="http://localhost:3000")
let result = await client.call(
Contract.createPost,
{
"title": "How to use ReScript Rest?",
"body": "Read the documentation on GitHub",
}
// ^-- Fully typed!
) // ℹ️ It'll do a POST request to http://localhost:3000/posts with application/json body
let result = await client.call(
Contract.getPost,
"123"
// ^-- Fully typed!
) // ℹ️ It'll do a GET request to http://localhost:3000/posts/123
let result = await client.call(
Contract.getPosts,
{
Expand All @@ -64,9 +95,7 @@ let result = await client.call(
"page": Some(1),
}
// ^-- Fully typed!
)
// ℹ️ It'll do a GET request to http://localhost:3000/posts?skip=0&take=10 with the `{"x-pagination-page": 1}` headers.
) // ℹ️ It'll do a GET request to http://localhost:3000/posts?skip=0&take=10 with the `{"x-pagination-page": 1}` headers
```

> 🧠 Currently `rescript-rest` supports only `client`, but the idea is to reuse the file both for `client` and `server`.
Expand All @@ -75,7 +104,7 @@ let result = await client.call(

- [x] Support query params
- [x] Support headers
- [ ] Support path params
- [x] Support path params
- [ ] Implement type-safe response
- [ ] Support custom fetch options
- [ ] Support non-json body
Expand Down
58 changes: 58 additions & 0 deletions __tests__/Rest_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,22 @@ asyncTest("Example test", async t => {
{body: JsonString("true"), status: 200}
})

let _createPost = Rest.route(() => {
path: "/posts",
method: "POST",
variables: s =>
{
"title": s.field("title", S.string),
"body": s.field("body", S.string),
},
})

let _getPost = Rest.route(() => {
path: "/posts/:id",
method: "GET",
variables: s => s.param("id", S.string),
})

let getPosts = Rest.route(() => {
path: "/posts",
method: "GET",
Expand All @@ -201,3 +217,45 @@ asyncTest("Example test", async t => {

t->ExecutionContext.plan(2)
})

asyncTest("Multiple path params", async t => {
let client = Rest.client(~baseUrl="http://localhost:3000", ~api=async (
args
): Rest.ApiFetcher.return => {
t->Assert.deepEqual(
args,
{
path: "http://localhost:3000/post/abc/comments/1/123",
body: None,
headers: None,
method: "GET",
},
)
{body: JsonString("true"), status: 200}
})

let getSubComment = Rest.route(() => {
path: "/post/:id/comments/:commentId/:commentId2",
method: "GET",
variables: s =>
{
"id": s.param("id", S.string),
"commentId": s.param("commentId", S.int),
"commentId2": s.param("commentId2", S.int),
},
})

t->Assert.deepEqual(
await client.call(
getSubComment,
{
"id": "abc",
"commentId": 1,
"commentId2": 123,
},
),
{body: JsonString("true"), status: 200},
)

t->ExecutionContext.plan(2)
})
4 changes: 2 additions & 2 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rescript-rest",
"version": "0.2.0",
"version": "0.3.0",
"description": "ReScript RPC-like client, contract, and server implementation for a pure REST API",
"keywords": [
"rest",
Expand Down
31 changes: 29 additions & 2 deletions src/Rest.res
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ type s = {
field: 'value. (string, S.t<'value>) => 'value,
header: 'value. (string, S.t<'value>) => 'value,
query: 'value. (string, S.t<'value>) => 'value,
param: 'value. (string, S.t<'value>) => 'value,
}
type routeDefinition<'variables> = {
method: string,
Expand Down Expand Up @@ -141,9 +142,31 @@ let rec tokeniseValue = (key, value, ~append) => {
}
}

// FIXME: Validate that all defined paths are registered
// FIXME: Prevent `/` in the path param
/**
* @param path - The URL e.g. /posts/:id
* @param maybeParams - The params e.g. `{ id: string }`
* @returns - The URL with the params e.g. /posts/123
*/
let insertParamsIntoPath = (~path, ~maybeParams) => {
path
->Js.String2.unsafeReplaceBy1(%re("/:([^/]+)/g"), (_, p, _, _) => {
switch maybeParams {
| Some(params) =>
switch params->Js.Dict.unsafeGet(p)->(Obj.magic: unknown => option<string>) {
| Some(s) => s
| None => ""
}
| None => ""
}
})
->Js.String2.replaceByRe(%re("/\/\//g"), "/")
}

// Inspired by https://github.com/ts-rest/ts-rest/blob/7792ef7bdc352e84a4f5766c53f984a9d630c60e/libs/ts-rest/core/src/lib/client.ts#L347
let getCompletePath = (~baseUrl, ~routePath, ~maybeQuery, ~jsonQuery) => {
let path = ref(baseUrl ++ routePath)
let getCompletePath = (~baseUrl, ~routePath, ~maybeQuery, ~maybeParams, ~jsonQuery) => {
let path = ref(baseUrl ++ insertParamsIntoPath(~path=routePath, ~maybeParams))

switch maybeQuery {
| None => ()
Expand Down Expand Up @@ -215,6 +238,9 @@ let client = (~baseUrl, ~api=ApiFetcher.default, ~jsonQuery=false) => {
query: (fieldName, schema) => {
s.nestedField("query", fieldName, schema)
},
param: (fieldName, schema) => {
s.nestedField("params", fieldName, schema)
},
})
})

Expand Down Expand Up @@ -251,6 +277,7 @@ let client = (~baseUrl, ~api=ApiFetcher.default, ~jsonQuery=false) => {
~baseUrl,
~routePath=definition.path,
~maybeQuery=data["query"],
~maybeParams=data["params"],
~jsonQuery,
),
method: definition.method,
Expand Down
23 changes: 20 additions & 3 deletions src/Rest.res.js

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

1 change: 1 addition & 0 deletions src/Rest.resi
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type s = {
field: 'value. (string, S.t<'value>) => 'value,
header: 'value. (string, S.t<'value>) => 'value,
query: 'value. (string, S.t<'value>) => 'value,
param: 'value. (string, S.t<'value>) => 'value,
}
type routeDefinition<'variables> = {
method: string,
Expand Down

0 comments on commit 1450277

Please sign in to comment.