Skip to content

Commit

Permalink
v0.2.0 🚀
Browse files Browse the repository at this point in the history
  • Loading branch information
DZakh committed Jun 19, 2024
1 parent 39574e4 commit a15aefa
Show file tree
Hide file tree
Showing 9 changed files with 334 additions and 123 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
lib
lib
__tests__/*.res.js
41 changes: 26 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,39 +35,50 @@ Add `rescript-rest` to `bs-dependencies` in your `rescript.json`:
}
```

## Basic usage
## Super Simple Example

Create `Contract.res` and define your routes:
Easily define your API contract somewhere shared, for example, `Contract.res`:

```rescript
// Contract.res
let createGame = Rest.route(() => {
path: "/game",
method: "POST",
let getPosts = Rest.route(() => {
path: "/posts",
method: "GET",
variables: s => {
"userName": s.field("user_name", S.string),
"skip": s.query("skip", S.int),
"take": s.query("take", S.int),
"page": s.header("x-pagination-page", S.option(S.int)),
},
})
```

> 🧠 Currently `rescript-rest` supports only `client`, but the idea is to reuse the file both for `client` and `server`.
Now you can use the contract to perform type-safe calls to your server:
Consume the api on the client with a RPC-like interface:

```rescript
// Client.res
let client = Rest.client(~baseUrl="http://localhost:3000")
let _ = await client.call(Contract.createGame, ~variables={"userName": "Dmitry"})
let result = await client.call(
Contract.getPosts,
~variables={
"skip": 0,
"take": 10,
"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.
```

> 🧠 Currently `rescript-rest` supports only `client`, but the idea is to reuse the file both for `client` and `server`.
## Planned features

- [x] Support query params
- [x] Support headers
- [ ] Support path params
- [ ] Implement type-safe response
- [ ] Support passing headers and fetch options
- [ ] Support custom fetch options
- [ ] Support non-json body
- [ ] Generate OpenAPI from Contract
- [ ] Generate Contract from OpenAPI
- [ ] Integrate with Fastify on server-side
140 changes: 140 additions & 0 deletions __tests__/Rest_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,143 @@ asyncTest("Test simple GET request", async t => {

t->ExecutionContext.plan(2)
})

asyncTest("Test query params encoding to path", async t => {
let getHeight = Rest.route(() => {
path: "/height",
method: "GET",
variables: s =>
{
"string": s.query("string", S.string),
"unit": s.query("unit", S.unit),
"null": s.query("null", S.null(S.string)),
"bool": s.query("bool", S.bool),
"int": s.query("int", S.int),
"array": s.query("array", S.array(S.string)),
"nan": s.query("nan", S.literal(%raw(`NaN`))),
"float": s.query("float", S.float),
"matrix": s.query("matrix", S.array(S.array(S.string))),
"arrayOfObjects": s.query(
"arrayOfObjects",
S.array(S.object(s => s.field("field", S.string))),
),
"encoded": s.query("===", S.string),
"trueString": s.query("trueString", S.literal("true")),
"nested": s.query(
"nested",
S.object(
s =>
{
"unit": s.field("unit", S.unit),
"nestedNested": s.nestedField("nestedNested", "field", S.string),
},
),
),
},
})

let variables = {
"string": "abc",
"unit": (),
"null": None,
"bool": true,
"int": 123,
"array": ["a", "b", "c"],
"nan": %raw(`NaN`),
"float": 1.2,
"matrix": [["a0", "a1"], ["b0"]],
"arrayOfObjects": ["v0", "v1"],
"encoded": "===",
"trueString": "true",
"nested": {
"unit": (),
"nestedNested": "nv",
},
}

let client = Rest.client(~baseUrl="http://localhost:3000", ~api=async (
args
): Rest.ApiFetcher.return => {
t->Assert.deepEqual(
args,
{
path: "http://localhost:3000/height?string=abc&null=&bool=true&int=123&array[0]=a&array[1]=b&array[2]=c&nan=NaN&float=1.2&matrix[0][0]=a0&matrix[0][1]=a1&matrix[1][0]=b0&arrayOfObjects[0][field]=v0&arrayOfObjects[1][field]=v1&%3D%3D%3D=%3D%3D%3D&trueString=true&nested[nestedNested][field]=nv",
body: None,
headers: None,
method: "GET",
},
)
{body: JsonString("true"), status: 200}
})

t->Assert.deepEqual(
await client.call(getHeight, ~variables),
{body: JsonString("true"), status: 200},
)

let jsonQueryClient = Rest.client(
~baseUrl="http://localhost:3000",
~api=async (args): Rest.ApiFetcher.return => {
t->Assert.deepEqual(
args,
{
path: "http://localhost:3000/height?string=abc&null=null&bool=true&int=123&array=%5B%22a%22%2C%22b%22%2C%22c%22%5D&nan=null&float=1.2&matrix=%5B%5B%22a0%22%2C%22a1%22%5D%2C%5B%22b0%22%5D%5D&arrayOfObjects=%5B%7B%22field%22%3A%22v0%22%7D%2C%7B%22field%22%3A%22v1%22%7D%5D&%3D%3D%3D=%3D%3D%3D&trueString=%22true%22&nested=%7B%22nestedNested%22%3A%7B%22field%22%3A%22nv%22%7D%7D",
body: None,
headers: None,
method: "GET",
},
)
{body: JsonString("true"), status: 200}
},
~jsonQuery=true,
)

t->Assert.deepEqual(
await jsonQueryClient.call(getHeight, ~variables),
{body: JsonString("true"), status: 200},
)

t->ExecutionContext.plan(4)
})

asyncTest("Example test", async t => {
let client = Rest.client(~baseUrl="http://localhost:3000", ~api=async (
args
): Rest.ApiFetcher.return => {
t->Assert.deepEqual(
args,
{
path: "http://localhost:3000/posts?skip=0&take=10",
body: None,
headers: %raw(`{"x-pagination-page": 1}`),
method: "GET",
},
)
{body: JsonString("true"), status: 200}
})

let getPosts = Rest.route(() => {
path: "/posts",
method: "GET",
variables: s =>
{
"skip": s.query("skip", S.int),
"take": s.query("take", S.int),
"page": s.header("x-pagination-page", S.option(S.int)),
},
})

t->Assert.deepEqual(
await client.call(
getPosts,
~variables={
"skip": 0,
"take": 10,
"page": Some(1),
},
),
{body: JsonString("true"), status: 200},
)

t->ExecutionContext.plan(2)
})
91 changes: 0 additions & 91 deletions __tests__/Rest_test.res.js

This file was deleted.

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.1.0",
"version": "0.2.0",
"description": "ReScript RPC-like client, contract, and server implementation for a pure REST API",
"keywords": [
"rest",
Expand Down
Loading

0 comments on commit a15aefa

Please sign in to comment.