Skip to content

Commit

Permalink
Merge pull request #15 from contactlab/next
Browse files Browse the repository at this point in the history
Version 1.0.0
  • Loading branch information
StefanoMagrassi authored Sep 24, 2018
2 parents 9f099e1 + e8d0e74 commit 653c7bc
Show file tree
Hide file tree
Showing 39 changed files with 7,292 additions and 4,907 deletions.
3 changes: 0 additions & 3 deletions .babelrc

This file was deleted.

14 changes: 14 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# http://editorconfig.org
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.md]
insert_final_newline = false
trim_trailing_whitespace = false
15 changes: 0 additions & 15 deletions .flowconfig

This file was deleted.

5 changes: 2 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
node_modules
.vscode
.nyc_output
*.log*
dist
coverage
lib
3 changes: 3 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
git-tag-version = false
tag-version-prefix =
save-exact = true
6 changes: 6 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"printWidth": 80,
"singleQuote": true,
"bracketSpacing": false,
"jsxBracketSameLine": false
}
4 changes: 3 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ language: node_js
node_js:
- "8"
- "node"
cache: yarn
cache:
directories:
- "node_modules"
1 change: 0 additions & 1 deletion .yarnrc

This file was deleted.

239 changes: 182 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,88 +1,213 @@
# Appy [![Build Status](https://travis-ci.org/contactlab/appy.svg?branch=master)](https://travis-ci.org/contactlab/appy)
# Appy

Fetch API the Contactlab way
[![Build Status](https://travis-ci.org/contactlab/appy.svg?branch=master)](https://travis-ci.org/contactlab/appy)

This package is statically typed using Flow
Fetch API the Contactlab way

## Install

```sh
$ yarn add @contactlab/appy
$ npm install @contactlab/appy

# --- or ---

$ yarn add @contactlab/appy
```

## Use
## Motivation

Appy try to offer a better model for fething resources, using the standard global `fetch()` function as a "backbone" and some principles from Functional Programming paradigm.

The model is built around the concepts of:

### Request
- asynchronous operations (`Task`)
- which can fail for some reason (`Either`)
- or return data with a specific shape that should be decoded/validated (`Decoder`).

```js
import {request} from '@contactlab/appy';
In order to achieve this, Appy intensely uses:

const options = {
// request options
};
- [Typescript](https://www.typescriptlang.org) >= v2.9
- [`fp-ts`](https://github.com/gcanti/io-ts)
- [`io-ts`](https://github.com/gcanti/fp-ts/)

// request :: (String, String, ?Object) => Promise<Object>
request('GET', 'https://my.api.com/me', options)
.then(handleResponse)
.catch(handleError)
## API

**Note:** every sub module/lib is exported into the main `index.ts` file for a comfortable use.

### request

```typescript
import {get} from '@contactlab/appy';
// same as:
// import {get} from '@contactlab/appy/lib/request';

get('http://jsonplaceholder.typicode.com/posts')
.run()
.then(result =>
result.fold(err => console.error(err), data => console.log(data))
);
```

Request `options`:
- There is no need to specify a `method` key, it is overridden by the method argument passed to `request()`.
- The default `mode` is set to "cors".
- The `body` content gets stringified, so objects are accepted
This is a low level module:
it uses the standard Web API Fetch function (`fetch`) in order to make a request to a resource
and wraps it in a `TaskEither` monad.

So, you can:

`request` returns a Promise.
For both Success and Reject cases the return type is:
- use the standard, clean and widely supported api to make XHR;
- "project" it into a declarative functional world where execution is lazy (`Task`);
- handle "by design" the possibility of a failure with an explicit channel for errors (`Either`).

```js
Promise<{
status: string,
payload: Object | {message: string}
}>
The module tries to be as more compliant as possible with the `fetch()` interface but with subtle differences:

- request `method` is always explicit (no implicit "GET");
- accepted methods are definened by the `Method` union type;
- `fetch`'s input is always a `USVString` (no `Request` objects allowed);
- `Response` is mapped into a specific `AppyResponse<Mixed>` interface;
- `AppyResponse` `headers` property is always a `HeadersMap` (alias for a map of string);
- `AppyResponse` has a `body` property that is the result of parsing to JSON the string returned from `response.text()`; if it cannot be parsed as JSON, `body` value is just the string (both types of data are covered by the `Mixed` type).

`RequestInit` configuration object instead remains the same.

#### Exports

See [here](src/request.ts) for the complete list of types.

```typescript
function request(
m: Method,
u: USVString,
o?: RequestInit
): TaskEither<AppyError, AppyResponse<Mixed>>;
```

### Api
```typescript
function get(
u: USVString,
o?: RequestInit
): TaskEither<AppyError, AppyResponse<Mixed>>;
```

Internally uses the `request()`.
```typescript
function post(
u: USVString,
o?: RequestInit
): TaskEither<AppyError, AppyResponse<Mixed>>;
```

```js
import {api} from '@contactlab/appy';
```typescript
function put(
u: USVString,
o?: RequestInit
): TaskEither<AppyError, AppyResponse<Mixed>>;
```

const config = {
baseUri: 'https://my.api.com',
id: 'module_id',
version: '1.0.0'
}
const options = {
// request options
};
```typescript
function patch(
u: USVString,
o?: RequestInit
): TaskEither<AppyError, AppyResponse<Mixed>>;
```

// api :: (Object) => (String, String, String, ?Object) => Promise<Object>
const myFetch = api(config);
myFetch('GET', '/me', 'myToken', options)
.then(handleResponse)
.catch(handleError)
```typescript
function del(
u: USVString,
o?: RequestInit
): TaskEither<AppyError, AppyResponse<Mixed>>;
```

Request `options` as above apart from the headers:
- If not overwritten by the headers passed in, `"Accept"` and `"Content-type"` are set to "application/json".
- `"Authorization"` is set by the `token` argument.
- `"Contactlab-ClientId"` is set by the `id` key of the configuration.
- `"Contactlab-ClientVersion"` is set by the `version` key of the configuration.
### api

```typescript
import * as t from 'io-ts';
import {api} from '@contactlab/appy';
// same as:
// import {api} from '@contactlab/appy/lib/api';

const myApi = api({baseUri: 'http://jsonplaceholder.typicode.com'});
const token = 'secret';
const Posts = t.array(
t.type({
userId: t.number,
id: t.number,
title: t.string,
body: t.string
})
);

myApi
.get('/posts', {token, decoder: Posts})
.run()
.then(result =>
result.fold(err => console.error(err), data => console.log(data))
);
```

`api` at the second call returns the `request` Promise type.
This module is tailored on the needs of the Contactlab Frontend Team.
It uses the "low-level" `request` module in order to interact with Contactlab's services (REST API).

Unless a configuration error occurs, in that case the return type will be:
So, it is a little more opinionated:

```js
Promise<{
error: string
}>
- the exposed function (`api`) is used to "load" some configuration and returns an object with methods;
- the configuration has a required `baseUri` (string) key which will be prepended to every `uri`;
- there are also 2 optional keys `id` (string) and `version` (string) which will be passed as request's `headers`:
- `'Contactlab-ClientId': ${id}`,
- `'Contactlab-ClientVersion': ${version}`;
- the main `api` method is `request()` which uses under the hood the `request` module with some subtle differences:
- the `options` parameter is mandatory and it is an extension of the `RequestInit` interface;
- `options` has a required `token` (string) key which will be passed as request's `Authorization: Bearer ${token}` header;
- `options` has a require `decoder` (`Decoder<Mixed, A>`) key which will be used to decode the service's JSON payload;
- decoder errors are expressed with a `DecoderError` class which extends the `AppyError` tagged union type;
- thus, the returned type of `api` methods is `TaskEither<ApiError, A>`
- `headers` in `options` object can only be a map of strings (`{[k: string]: string}`); if you need to work with a `Header` object you have to transform it;
- `options` is merged with a predefined object in order to set some default values:
- `mode: 'cors'`
- `headers: {'Accept': 'application/json', 'Content-type': 'application/json'}`

#### Exports

See [here](src/api.ts) for the complete list of types.

```typescript
interface ApiMethods {
request: <A>(m: Method, u: USVString, o: ApiOptions<A>): TaskEither<ApiError, AppyResponse<A>>;

get: <A>(u: USVString, o: ApiOptions<A>): TaskEither<ApiError, AppyResponse<A>>;

post: <A>(u: USVString, o: ApiOptions<A>): TaskEither<ApiError, AppyResponse<A>>;

put: <A>(u: USVString, o: ApiOptions<A>): TaskEither<ApiError, AppyResponse<A>>;

patch: <A>(u: USVString, o: ApiOptions<A>): TaskEither<ApiError, AppyResponse<A>>;

del: <A>(u: USVString, o: ApiOptions<A>): TaskEither<ApiError, AppyResponse<A>>;
}

function api(c: ApiConfig): ApiMethods
```

### Test
## Examples

You can find a couple of examples [here](examples).

## About `fetch()` compatibility

The Fetch API is available only on "modern" browsers: if you need to support legacy browsers (e.g. **Internet Explorer 11** or older) or you want to use it in a Nodejs script we recommend you the excellent [`isomorphic-fetch`](https://www.npmjs.com/package/isomorphic-fetch) package.

## Contributing

Opening issues is always welcome.

Then, fork the repository or create a new branch, write your code and send a pull request.

This project uses [Prettier](https://prettier.io/) (automatically applied as pre-commit hook), [TSLint](https://palantir.github.io/tslint/) and [Jest](https://facebook.github.io/jest/en/).

Tests are run with:

```sh
$ yarn test
```
$ npm test
```

## License

Released under the [Apache 2.0](LICENSE) license.
7 changes: 7 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Examples

In order to show the examples resultes go to project root and run

```sh
$ npx ts-node ./examples/${EXAMPLE_FILE_NAME}
```
39 changes: 39 additions & 0 deletions examples/get-and-post-album.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*tslint:disable:no-console*/

/**
* Fetch a list of albums with a simple request,
* take the first,
* take the title,
* create a new album with the title in upper-case
* and print the result
*/

import 'isomorphic-fetch';
import {get, post} from '../src/index';

interface Album {
userId: number;
id: number;
title: string;
}

const URL = 'http://jsonplaceholder.typicode.com/albums';

// same as: request('GET', URL)
get(URL)
.map(data => (data.body as Album[])[0]) // this is not safe ;)
.map(album => ({
id: 1001,
userId: album.userId,
title: album.title.toUpperCase()
}))
.chain(newAlbum =>
post(URL, {
headers: {'Content-type': 'application/json'},
body: JSON.stringify(newAlbum)
})
)
.run()
.then(result =>
result.fold(err => console.log(err), data => console.log(data.body))
);
Loading

0 comments on commit 653c7bc

Please sign in to comment.