Skip to content

Commit

Permalink
v0.9.0 🚀 - Helper for Authentication header
Browse files Browse the repository at this point in the history
  • Loading branch information
DZakh committed Sep 3, 2024
1 parent a100a19 commit 30096f7
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 6 deletions.
16 changes: 14 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,18 +153,30 @@ You can also configure rescript-rest to encode/decode query parameters as JSON b

You can add headers to the request by using the `s.header` method in the `variables` definition.

### Authentication header

For the Authentication header there's an additional helper `s.auth` which supports `Bearer` and `Basic` authentication schemes.

```rescript
let getPosts = Rest.route(() => {
path: "/posts",
method: Get,
variables: s => {
"authorization": s.header("authorization", S.string),
"pagination": s.header("pagination", S.option(S.int)),
"token": s.auth(Bearer),
"pagination": s.header("x-pagination", S.option(S.int)),
},
responses: [
s => s.data(S.array(postSchema)),
],
})
let result = await client.call(
getPosts,
{
"token": "abc",
"pagination": 10,
}
) // ℹ️ It'll do a GET request to http://localhost:3000/posts with the `{"authorization": "Bearer abc", "x-pagination": "10"}` headers
```

## Raw Body
Expand Down
100 changes: 100 additions & 0 deletions __tests__/Rest_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,106 @@ asyncTest("Test request with mixed body and header data", async t => {
t->ExecutionContext.plan(3)
})

asyncTest("Test request with Bearer auth", async t => {
let createGame = Rest.route(() => {
path: "/game",
method: Post,
variables: s =>
{
"userName": s.field("user_name", S.string),
"bearer": s.auth(Bearer),
},
responses: [
s => {
s.status(#200)
s.data(S.bool)
},
],
})

let app = Fastify.make()
app->Fastify.route(createGame, async variables => {
t->Assert.deepEqual(
variables,
{
"userName": "Dmitry",
"bearer": "abc",
},
)
true
})

let client = Rest.client(~baseUrl="http://localhost:3000", ~fetcher=args => {
t->Assert.deepEqual(
args,
{
path: "http://localhost:3000/game",
body: `{"user_name":"Dmitry"}`->Obj.magic,
headers: %raw(`{
"content-type": "application/json",
"authorization": "Bearer abc"
}`),
method: "POST",
},
)
app->inject(args)
})

t->Assert.deepEqual(await client.call(createGame, {"userName": "Dmitry", "bearer": "abc"}), true)

t->ExecutionContext.plan(3)
})

asyncTest("Test request with Basic auth", async t => {
let createGame = Rest.route(() => {
path: "/game",
method: Post,
variables: s =>
{
"userName": s.field("user_name", S.string),
"token": s.auth(Basic),
},
responses: [
s => {
s.status(#200)
s.data(S.bool)
},
],
})

let app = Fastify.make()
app->Fastify.route(createGame, async variables => {
t->Assert.deepEqual(
variables,
{
"userName": "Dmitry",
"token": "abc",
},
)
true
})

let client = Rest.client(~baseUrl="http://localhost:3000", ~fetcher=args => {
t->Assert.deepEqual(
args,
{
path: "http://localhost:3000/game",
body: `{"user_name":"Dmitry"}`->Obj.magic,
headers: %raw(`{
"content-type": "application/json",
"authorization": "Basic abc"
}`),
method: "POST",
},
)
app->inject(args)
})

t->Assert.deepEqual(await client.call(createGame, {"userName": "Dmitry", "token": "abc"}), true)

t->ExecutionContext.plan(3)
})

asyncTest("Test simple GET request", async t => {
let getHeight = Rest.route(() => {
path: "/height",
Expand Down
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.8.0",
"version": "0.9.0",
"description": "😴 ReScript RPC-like client, contract, and server implementation for a pure REST API",
"keywords": [
"rest",
Expand Down
37 changes: 37 additions & 0 deletions src/Rest.res
Original file line number Diff line number Diff line change
Expand Up @@ -207,13 +207,16 @@ type pathParam = {name: string}
@unboxed
type pathItem = Static(string) | Param(pathParam)

type auth = Bearer | Basic

type s = {
field: 'value. (string, S.t<'value>) => 'value,
body: 'value. S.t<'value> => 'value,
rawBody: 'value. 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,
auth: auth => string,
}

type method =
Expand Down Expand Up @@ -312,6 +315,30 @@ let coerceSchema = schema => {
})
}

let bearerAuthSchema = S.string->S.transform(s => {
serializer: token => {
`Bearer ${token}`
},
parser: string => {
switch string->Js.String2.split(" ") {
| ["Bearer", token] => token
| _ => s.fail("Invalid Bearer token")
}
},
})

let basicAuthSchema = S.string->S.transform(s => {
serializer: token => {
`Basic ${token}`
},
parser: string => {
switch string->Js.String2.split(" ") {
| ["Basic", token] => token
| _ => s.fail("Invalid Basic token")
}
},
})

let params = route => {
switch (route->Obj.magic)["_rest"]->(
Obj.magic: unknown => option<routeParams<'variables, 'response>>
Expand Down Expand Up @@ -361,6 +388,16 @@ let params = route => {
}
s.nestedField("params", fieldName, coerceSchema(schema))
},
auth: auth => {
s.nestedField(
"headers",
"authorization",
switch auth {
| Bearer => bearerAuthSchema
| Basic => basicAuthSchema
},
)
},
})
})

Expand Down
47 changes: 46 additions & 1 deletion src/Rest.res.js

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

3 changes: 3 additions & 0 deletions src/Rest.resi
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,16 @@ module Response: {
}
}

type auth = Bearer | Basic

type s = {
field: 'value. (string, S.t<'value>) => 'value,
body: 'value. S.t<'value> => 'value,
rawBody: 'value. 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,
auth: auth => string,
}

type method =
Expand Down

0 comments on commit 30096f7

Please sign in to comment.