Skip to content
This repository has been archived by the owner on Sep 18, 2024. It is now read-only.

Commit

Permalink
added documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
poef committed Feb 9, 2024
1 parent 2f0a241 commit 67b3385
Show file tree
Hide file tree
Showing 19 changed files with 392 additions and 0 deletions.
56 changes: 56 additions & 0 deletions docs/asserts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Asserts

## Asserting preconditions

When writing middleware there is usually quite a lot of preconditions to check. When a developer wants to use your middleware, it is nice to have explicit feedback about what he or she is doing wrong. However this is only useful during development. Once in production you should assume that there are no developer mistakes anymore... or at least that the end user has no use for detailed error reports about your middleware.

This is especially true about mock middleware. Mock middleware is middleware that blocks the actual transmission of a request, and returns a mock response instead. Your browser doesn't actually fetch the requests URL.

The [oauth2 middleware]() for example, has unit tests that use the [oauth2 mock middleware]() to mimick a server. This way you can be sure that the oauth2 client implementation works, without having to setup a real oauth2 server anywhere.

Since these mock middleware servers are especially meant for the initial development of new middleware, they should assert as much as they can. And send comprehensive error messages to the console. Here the [`assert.fails()`](./reference/assert/fails.md) method comes in handy.

assert.fails() returns false if there are no problems. If one or more assertion does fail, it will return an array with messages about each failed assertion. So one way of using it is like this:

```javascript
let error

if (error = assert.fails(url, {
searchParams: {
response_type: 'code',
client_id: 'mockClientId',
state: assert.optional(/.+/)
}
})) {
return metro.response({
url: req.url,
status: 400,
statusText: 'Bad Request',
body: '400 Bad Request'
})
}
```

The first parameter to assert.fails contains the data you want to check. The second (or third, fourth, etc.) contain the assertions. If the data is an object, the assertions can use the same property names to add assertions for those specific properties. Here the url.searchParams.response_type must be equal to 'code', or the assertion will fail. You can also use numbers and booleans like this.

You can also add functions to the assertions. In this case the assert.optional() method adds a function that will only fail if the property is set and not null, but does not match the assertions passed to assert.optional().

An assertion may also be a regular expression. If the property value fails to match that expression, the assertion fails. Here the url.searchParams.state is tested to make sure that, if it is set, it must not be empty.

In a mock middleware function, it is all well and good to always test your preconditions. But in production many preconditions may be assumed to be valid. These preconditions are not expected to fail in production, only in development. In that case you may use assert.check(). This function by default does nothing. Only when you enable assertions does this function do anything. This allows you to selectively turn on assertions only in a development context. And avoid doing unnecessary work while in production. This is how it is used in the oauth2 middleware (not the mock server, the actual client code):

```javascript
assert.check(oauth2, {
client_id: /.+/,
authRedirectURL: /.+/,
scope: /.*/
})
```

This makes sure that the client_id and authRedirectURL configuration options have been set and are not empty. But when the code is used in production, this should never happen. There is no need to constantly test for this. And in production it won't actually get checked. Only when you enable assertions will this code actually perform the tests:

```javascript
metro.assert.enable()
```

Once the [`assert.enable()`](./reference/assert/enable.md) function is called, now assert.check will throw an error if any assertion fails. The error is also logged to the console.
36 changes: 36 additions & 0 deletions docs/debugging.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Debugging

## Introduction

Adding middleware to the fetch()--client.get(), client.post(), etc..--call hides a lot of complexity, which is good. But it also makes debugging harder. So MetroJS adds a number of tools to make debugging easier.

The most important of these are tracers.

## Tracing middleware

Tracers allow you to look under the hood and see what is going on in each middleware function. Every time a middleware function is called, the client checks if any tracers are set. If so, the request tracer is called before the middleware function, and the response tracer is called after the middleware function.

A tracer function cannot alter the request or response. But you can use it to log information. Or you can set a debugger trap if a specific condition is met, e.g:

```javascript
metro.trace.add('debug', {
request: (req) => {
if (req.searchParams.has('foo')) {
debugger;

}

}
})
```

A tracer is an object with at most two functions, named 'request' and 'response'. You don't have to specify both of them. A tracer function doesn't return anything. It can not change the request or response.

You can add more than one tracer. Each name must be unique. You can remove a tracer by name, or clear all tracers. Tracers are stored globally, and run on any metro client request.

There is a default tracer method included with MetroJS, called metro.trace.group. You can add it like this:

```javascript
metro.trace.add('group', metro.trace.group())
```

32 changes: 32 additions & 0 deletions docs/middleware.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Middleware

## Using middleware

You can add middleware to a client using the client() function directly, or by calling with() on an existing client. This will return a new client with the middleware added:

```javascript
import jsonmw from '@muze-nl/metro/src/mw/jsonmw'
const client = metro.client( jsonmw() )
```

See the [reference]() for a list of [default middlewares]() available with MetroJS.

## Creating middleware

A middleware is a function with (request, next) as parameters, returning a response. Both request and response adhere to the Fetch API Request and Response standard.

next is a function that takes a request and returns a Promise<Response>. This function is defined by MetroJS and automatically passed to your middleware function. The idea is that your middleware function can change the request and pass it on to the next middleware or the actual fetch() call, then intercept the response and change that and return it:

```javascript
async function myMiddleware(req,next) {
req = req.with('?foo=bar')
let res = await next(req)
if (res.ok) {
res = res.with({headers:{'X-Foo':'bar'}})
}
return res
}
```

/Note/: Both request and response have a with function. This allows you to create a new request or response, from the existing one, with one or more options added or changed. The original request or response is not changed. See the [reference]() for more information.

12 changes: 12 additions & 0 deletions docs/reference/assert/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Assert

The assert library allows you to add assertion checks in your code, e.g. middleware components.
Assertion checking can be turned on and off globally, so you can enable it in a development setting, but disable it in production.

## Methods
- [`assert.check`](./check.md)
- [`assert.disable`](./disable.md)
- [`assert.enable`](./enable.md)
- [`assert.fails`](./fails.md)
- [`assert.oneOf`](./oneOf.md)
- [`assert.optional`](./optional.md)
7 changes: 7 additions & 0 deletions docs/reference/assert/check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# assert.check

```
assert.check(data, ...assertions) : throws
```

This will call [`assert.fails()`](./fails.md). If any assertion fails, it will throw an error with all failed assertions. If assert is disabled--the default state--no assertions will be checked. See [`assert.fails`](./fails.md) for a list of possible assertions.
7 changes: 7 additions & 0 deletions docs/reference/assert/disable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# assert.disable

```
assert.disable()
```

Will stop validation by [`assert.check()`](./check.md). This is the default state.
7 changes: 7 additions & 0 deletions docs/reference/assert/enable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# assert.enable

```
assert.enable()
```

Will enable assertion validation by [`assert.check()`](./check.md). You can disable assertion at any point by calling [`assert.disable()`](./disable.md).
27 changes: 27 additions & 0 deletions docs/reference/assert/fails.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# assert.fails

```
assert.fails(data, ...assertions) : <false|Array>
```

This checks if data matches all of the assertions. If any assertion fails, this will return an array of failed assertions.

If data is an object, assertions must also be an object. For any property of data, you can add the same property to an assertion object. That assertion can be:

a string, number or boolean: the property in the data must be the same (== comparison)
a regular expression: the property in the data must match this expression
a function: the function is called with (property, data) and must return false (for success) or an array of problems.
an object: each of the properties of this object must match with the child properties of the data

Here is an example:

```
let errors = assert.fails(response, {
status: 200,
headers: {
'Content-Type':'application/json',
'Etag': assert.optional(/([a-z0-9_\-])+/i)
},
body: myValidatorFunction
})
```
15 changes: 15 additions & 0 deletions docs/reference/assert/oneOf.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# assert.oneOf

```
assert.oneOf(...assertions): Function
```

This function is meant to be used as part of an assertion in [`assert.fails()`](./fails.md), e.g:

```javascript
let errors = assert.fails(url.searchParams, {
grant_type: assert.oneOf('refresh_token','authorization_code')
})
```

Here it will fail if the search parameter `grant_type` is anything other than either `'refresh_token'` or `'authorization_code'`.
17 changes: 17 additions & 0 deletions docs/reference/assert/optional.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# assert.optional

```
assert.optional(...assertions): Function
```

This function is meant to be used as part of an assertion in [`assert.fails()`](./fails.md), e.g:

```javascript
let errors = assert.fails(url.searchParams, {
state: assert.optional(/.+/)
})
```

Here it will only fail if the search parameter state is set, but doesn't match the given regular expression.

`assert.optional()` returns a function that is used by [`assert.fails`](.fails.md). It will return `false` if there are no problems.
70 changes: 70 additions & 0 deletions docs/reference/client/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Client

```
metro.client(...options) : Client
```

Returns a new client, with a default request object built out of the options passed. Later option values in the parameter list override earlier values.

You can pass any option that would be valid for the default [Request constructor](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request). You can also provide a [metro request](../request/README.md) as option. Any option specified will result in all subsequent HTTP calls having those options by default. You can still override them.

Where [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/fetch) uses the first parameter to specify the URL, you can use any position in the parameters for `metro.client()` to pass a URL. Either by using the [`metro.url()`](../url/README.md) function to create a specific URL object, or the browsers default [`URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL) class, or a [Location](https://developer.mozilla.org/en-US/docs/Web/API/Location) object, like `document.location`. Or by just using a string. Any string that is passed as an option is assumed to be a URL.

## with

You can create a new client, with one or more options set to a new default value, by calling the [`with()`](./with.md) method.

```javascript
const newClient = oldClient.with({
url: '/otherpath/'
})
````

## Middleware

In addition you can also add a middleware function with this signature:

```
async (req, next) => Response
```

Where `req` is a [metro request](../request/README.md), which is a [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) for the default [Request class](https://developer.mozilla.org/en-US/docs/Web/API/Request). See below. `next` is an asynchronous function that takes a request and returns a Promise\<Response>. You must call this function yourself in your middleware function. Here is an example that just adds an authorization header to each request:

```javascript
const client = metro.client(async (req,next) => {
return next(req.with({
headers: {
'Authorization':'Bearer '+token
}
}))
})
```

## HTTP Methods

By default a metro client will have the methods: [`get()`](./get.md), [`post()`](./post.md), [`put()`](./put.md), [`delete()`](./delete.md), [`options()`](./options.md), [`patch()`](./patch.md) and [`query()`](./query.md). These all correspond with the similarly named HTTP methods, or verbs.

You can restrict, or enhance this list by setting the verbs property, like this:

```javascript
const client = metro.client({
verbs: ['get','post']
})
```

This will result in a client with only the `get()` and `post()` methods. Any word you add in this list, will be made available as a method, and will use the all uppercase word as the HTTP method.



## Client Methods

- [with](./with.md)

## Default HTTP Methods
- [get](./get.md)
- [post](./post.md)
- [put](./put.md)
- [delete](./delete.md)
- [patch](./patch.md)
- [options](.options.md)
- [query](./query.md)
9 changes: 9 additions & 0 deletions docs/reference/client/delete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# client.delete

```
client.delete(...options) : Promise<Response> : throws
```

This method is available by default, but can be disable. The `delete()` method will start a `HTTP DELETE` request, using [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/fetch), with the given options. It will return a [Promise]()https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise which resolves in a [Response](../response/README.md) on success.

This method is fully backwards compatible with the `fetch()` method, except the HTTP method is fixed to `DELETE`.
17 changes: 17 additions & 0 deletions docs/reference/client/get.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# client.get

```
client.get(...options) : Promise<Response>
```

This method is available by default, but can be disable. The `get()` method will start a `HTTP GET` request, using [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/fetch), with the given options. It will return a [Promise]()https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise which resolves in a [Response](../response/README.md) on success.

This method is fully backwards compatible with the `fetch()` method, except the HTTP method is fixed to `GET`.

In addition, it uses the defaults set when creating the client as the starting options. Options passed to `client.get()` can override those defaults.

For example: The client will have a default URL, usually that is the `document.location.href` in the browser. You can then set a relative URL in the get call, e.g.:

```javascript
let response = await client.get('/mypath/')
```
11 changes: 11 additions & 0 deletions docs/reference/client/options.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# client.options

```
client.options(...options) : Promise<Response> : throws
```

This method is available by default, but can be disable. The `options()` method will start a `HTTP OPTIONS` request, using [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/fetch), with the given options. It will return a [Promise]()https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise which resolves in a [Response](../response/README.md) on success.

This method is fully backwards compatible with the `fetch()` method, except the HTTP method is fixed to `OPTIONS`.

Just like [`client.post()`](./post.md) you can set a `body` parameter in the options.
11 changes: 11 additions & 0 deletions docs/reference/client/patch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# client.patch

```
client.patch(...options) : Promise<Response> : throws
```

This method is available by default, but can be disable. The `patch()` method will start a `HTTP PATCH` request, using [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/fetch), with the given options. It will return a [Promise]()https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise which resolves in a [Response](../response/README.md) on success.

This method is fully backwards compatible with the `fetch()` method, except the HTTP method is fixed to `PATCH`.

Just like [`client.post()`](./post.md) you can set a `body` parameter in the options.
29 changes: 29 additions & 0 deletions docs/reference/client/post.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# client.post

```
client.post(...options) : Promise<Response> : throws
```

This method is available by default, but can be disable. The `get()` method will start a `HTTP POST` request, using [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/fetch), with the given options. It will return a [Promise]()https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise which resolves in a [Response](../response/README.md) on success.

This method is fully backwards compatible with the `fetch()` method, except the HTTP method is fixed to `POST`.

In addition, it uses the defaults set when creating the client as the starting options. Options passed to `client.post()` can override those defaults.

If you pass a [FormData](../formdata/README.md) object as one of the options, it will automatically be send along as the POST body, using the default `application/x-www-form-urlencoded` encoding. You can change this by setting the appropriate `Content-Type` header, e.g. to `multipart/formdata`.

You can explicitly set the body of the request, e.g:

```javascript
let response = await client.post({
body: {
key: value
}
})
```

If the body property is one of these object types: String, ReadableStream, Blob, ArrayBuffer, DataView, FormData, URLSearchParams, then the body is passed on as-is to [fetch()]() and the browser handles it as normal.

If the body property is any other kind of object, the `post()` method will convert it to JSON by default.

Only the POST, PUT, PATCH and QUERY methods accept a body in the request. Other methods will throw an error.
Loading

0 comments on commit 67b3385

Please sign in to comment.