├── .all-contributorsrc
├── .babelrc
├── .gitignore
├── .npmignore
├── CONTRIBUTING.md
├── README.md
├── circle.yml
├── package.json
├── src
├── index.js
├── partial.js
├── request.js
└── utilities
│ ├── body.js
│ ├── fetch.js
│ └── normalize.js
└── tests
├── setup.js
└── unit
├── body.js
├── fetch.js
├── normalize.js
├── partial.js
└── request.js
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "legible",
3 | "projectOwner": "Legitcode",
4 | "files": [
5 | "README.md"
6 | ],
7 | "imageSize": 100,
8 | "commit": true,
9 | "contributors": [
10 | {
11 | "login": "zackify",
12 | "name": "Zach Silveira",
13 | "avatar_url": "https://avatars.githubusercontent.com/u/449136?v=3",
14 | "profile": "http://reactjsnews.com",
15 | "contributions": [
16 | "code",
17 | "doc",
18 | "review"
19 | ]
20 | },
21 | {
22 | "login": "raygesualdo",
23 | "name": "Ray Gesualdo",
24 | "avatar_url": "https://avatars.githubusercontent.com/u/5465958?v=3",
25 | "profile": "https://github.com/raygesualdo",
26 | "contributions": [
27 | "code",
28 | "doc"
29 | ]
30 | }
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "latest"
4 | ],
5 | "plugins": [
6 | "transform-object-rest-spread",
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | src
2 | tests
3 | .babelrc
4 | .gitignore
5 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thanks for being willing to contribute!
4 |
5 | **Working on your first Pull Request?** You can learn how from this *free* series [How to Contribute to an Open Source Project on GitHub][egghead]
6 |
7 | ## Project setup
8 |
9 | 1. Fork and clone the repo
10 | 2. `$ npm install` to install dependencies
11 | 3. Create a branch for your PR
12 |
13 | You can run `npm run` to see what scripts are available.
14 |
15 | ## Add yourself as a contributor
16 |
17 | This project follows the [all contributors][all-contributors] specification. To add yourself to the table of contributors on the README.md, please use the automated script as part of your PR:
18 |
19 | ```console
20 | npm run add-contributor
21 | ```
22 |
23 | Follow the prompt. If you've already added yourself to the list and are making a new type of contribution, you can run it again and select the added contribution type.
24 |
25 | [egghead]: https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github
26 | [all-contributors]: https://github.com/kentcdodds/all-contributors
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://circleci.com/gh/Legitcode/legible)
2 | [](#contributors)
3 | ## Legible
4 |
5 | [See my blog post on why I made this](https://zach.codes/human-readable-ajax-requests/)
6 |
7 | Proof of concept at making http requests easier to work with in JS / Node. This wraps the fetch api.
8 |
9 | ```
10 | npm install legible --save
11 | ```
12 |
13 | A request library using template literals. Making requests has never been so straight forward! Make it easy for users to adopt your api, document it using this library, and everyone will understand making requests.
14 |
15 | ### Example
16 |
17 | ```js
18 | import request from 'legible'
19 |
20 | async function TestRequest() {
21 | let body = {
22 | email: 'test@test.com',
23 | password: 'secret'
24 | }
25 |
26 | let response = await request`
27 | url: https://api.myapp.com/register
28 | method: POST
29 | body: ${body}
30 | headers: ${{
31 | Authorization: 'Bearer: token'
32 | }}
33 | `
34 | }
35 | ```
36 |
37 | ## Partial Requests
38 |
39 | **New in 0.2.0!**
40 |
41 |
42 | Using template strings, we can pull out variables easily and keep requests as `legible` as possible. Imagine splitting out your code like this using api libraries that include requests like so:
43 |
44 | ```js
45 | import { partial } from 'legible'
46 |
47 | const twitter = {
48 | register: partial`
49 | url: https://api.twitter.com/register,
50 | method: POST
51 | `
52 | }
53 |
54 | twitter.register`
55 | body: ${{
56 | email: 'test@test.com',
57 | password: 'Tester'
58 | }}
59 | `
60 | ```
61 |
62 | ### Middleware
63 |
64 | **Coming Soon** The following isn't implemented yet.
65 |
66 |
67 | ```js
68 | import request from 'legible'
69 |
70 | request.middleware({
71 | headers: {
72 | Authorization: `Bearer: ${localStorage.getItem('token')}`
73 | },
74 | after({ headers }) {
75 | localStorage.setItem('token', headers.Authorization)
76 | }
77 | })
78 | ```
79 |
80 | ## Contributors
81 |
82 | Thanks goes to these wonderful people ([emoji key](https://github.com/kentcdodds/all-contributors#emoji-key)):
83 |
84 |
85 | | [
Zach Silveira](http://reactjsnews.com)
[💻](https://github.com/Legitcode/legible/commits?author=zackify) [📖](https://github.com/Legitcode/legible/commits?author=zackify) 👀 | [
Ray Gesualdo](https://github.com/raygesualdo)
[💻](https://github.com/Legitcode/legible/commits?author=raygesualdo) [📖](https://github.com/Legitcode/legible/commits?author=raygesualdo) |
86 | | :---: | :---: |
87 |
88 |
89 | This project follows the [all-contributors](https://github.com/kentcdodds/all-contributors) specification. Contributions of any kind welcome!
90 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | machine:
2 | node:
3 | version: 7.0.0
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "legible",
3 | "version": "0.2.12",
4 | "description": "cleanly code your api requests",
5 | "main": "dist/index.js",
6 | "scripts": {
7 | "lint": "standard",
8 | "test": "npm run lint && mocha --require babel-register --require tests/setup tests/unit --recursive",
9 | "build": "babel src --out-dir dist",
10 | "add-contributor": "all-contributors add",
11 | "generate-contributors": "all-contributors generate"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "git+ssh://git@github.com/zackify/legible.git"
16 | },
17 | "keywords": [
18 | "request",
19 | "fetch",
20 | "http"
21 | ],
22 | "author": "Zach Silveira",
23 | "license": "ISC",
24 | "bugs": {
25 | "url": "https://github.com/zackify/legible/issues"
26 | },
27 | "homepage": "https://github.com/zackify/legible#readme",
28 | "devDependencies": {
29 | "all-contributors-cli": "^3.0.7",
30 | "babel-cli": "^6.18.0",
31 | "babel-plugin-transform-object-rest-spread": "^6.20.2",
32 | "babel-polyfill": "^6.20.0",
33 | "babel-preset-latest": "^6.16.0",
34 | "babel-register": "^6.18.0",
35 | "chai": "^3.5.0",
36 | "form-data": "^2.1.2",
37 | "mocha": "^3.2.0",
38 | "standard": "^8.6.0"
39 | },
40 | "dependencies": {
41 | "isomorphic-fetch": "^2.2.1"
42 | },
43 | "standard": {
44 | "globals": [
45 | "describe",
46 | "it",
47 | "expect",
48 | "FormData"
49 | ]
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import request from './request'
2 | import partialRequest from './partial'
3 |
4 | export default request
5 | export const partial = partialRequest
6 |
--------------------------------------------------------------------------------
/src/partial.js:
--------------------------------------------------------------------------------
1 | import fetch from './utilities/fetch'
2 | import normalize from './utilities/normalize'
3 |
4 | /*
5 | defines a partial request,
6 | returns a new function that merges any values
7 | */
8 |
9 | export default (strings, ...vars) => {
10 | let partial = normalize(strings, vars)
11 |
12 | return (strings, ...vars) => {
13 | let { options, url } = normalize(strings, vars, partial)
14 |
15 | // block the request if a url callback returns false
16 | if (url === false) return new Promise(resolve => resolve({ requestBlocked: true }))
17 |
18 | let headers = { ...partial.options.headers, ...options.headers }
19 | let mergedOptions = { ...partial.options, ...options, ...{ headers } }
20 | let finalUrl = url || partial.url
21 |
22 | return fetch(finalUrl, mergedOptions)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/request.js:
--------------------------------------------------------------------------------
1 | import fetch from './utilities/fetch'
2 | import normalize from './utilities/normalize'
3 |
4 | export default (strings, ...vars) => {
5 | let { options, url } = normalize(strings, vars)
6 | if (!options.method) options.method = 'GET'
7 | return fetch(url, options)
8 | }
9 |
--------------------------------------------------------------------------------
/src/utilities/body.js:
--------------------------------------------------------------------------------
1 | export default (body) => {
2 | // Handle empty case
3 | if (!body) return null
4 | // Handle FormData
5 | if (typeof FormData !== 'undefined' && body instanceof FormData) return body
6 | try {
7 | // Handle already stringified JSON
8 | JSON.parse(body)
9 | return body
10 | } catch (err) {
11 | // Handle plain object
12 | return JSON.stringify(body)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/utilities/fetch.js:
--------------------------------------------------------------------------------
1 | import fetch from 'isomorphic-fetch';
2 |
3 | export default (url, options = {}) => {
4 | return new Promise((resolve, reject) => {
5 | fetch(url, options)
6 | .then(response => {
7 | if (options.onResponse && options.onResponse(response)) return false;
8 |
9 | response
10 | .text()
11 | .then(text => {
12 | try {
13 | resolve(JSON.parse(text));
14 | } catch (e) {
15 | resolve({});
16 | }
17 | })
18 | .catch(function(error) {
19 | return reject(error);
20 | });
21 | })
22 | .catch(function(error) {
23 | return reject(error);
24 | });
25 | });
26 | };
27 |
--------------------------------------------------------------------------------
/src/utilities/normalize.js:
--------------------------------------------------------------------------------
1 | import processBody from './body'
2 |
3 | /*
4 | Methods that can be callbacks, ex:
5 | headers: ${partialHeaders => }
6 | */
7 | const callbackMethods = {
8 | headers: ({ value, partial }) => value(partial),
9 | url: ({ value, partial }) => value(partial.url)
10 | }
11 |
12 | /*
13 | Take in raw query string and
14 | return a fetch api compatible object
15 | */
16 | const buildObjectFromTag = (strings, vars, partial) => {
17 | const namespace = 'legible-request-var-'
18 | return strings
19 | // First, add namespaced placeholders to elements in `strings`
20 | // for each element in `vars`
21 | .map((str, index) => str + (vars[index] ? `${namespace}${index}` : ''))
22 | // Join the elements into a single string
23 | .join('')
24 | // Split them back out by linebreak
25 | .split('\n')
26 | // Remove empty elements
27 | .filter(i => i)
28 | // Trim each element
29 | .map(s => s.trim())
30 | // Split each element at `:`
31 | .map(s => s.split(':').map(s => s.trim()))
32 | // Strings from above step with multiple `:` in them will get split
33 | // more than once, meaning `values` is an array of strings instead of
34 | // a single string. Let's fix that.
35 | .map(([key, ...values]) => [key, values.join(':')])
36 | // If `value` is a reference, replace it with respective element in `vars`
37 | .map(([key, value]) => {
38 | // Ignore non-namespaced value
39 | if (!value.startsWith(namespace)) return [key, value]
40 | // Get the index at the end of the namespaced string
41 | const index = parseInt(value.replace(namespace, ''), 10)
42 |
43 | // run through any callback methods
44 | if (callbackMethods[key] && typeof vars[index] === 'function') {
45 | return [key, callbackMethods[key]({
46 | value: vars[index],
47 | partial
48 | })]
49 | }
50 |
51 | return [key, vars[index]]
52 | })
53 | // Convert to object
54 | .reduce((obj, [key, value]) => {
55 | return {...obj, [key]: value}
56 | }, {})
57 | }
58 |
59 | export default (strings, vars, partial = {}) => {
60 | const { url, method, body, ...options } = buildObjectFromTag(strings, vars, partial)
61 |
62 | let data = {
63 | url,
64 | options: {
65 | method,
66 | ...options,
67 | body: processBody(body)
68 | }
69 | }
70 | if (!data.options.method) delete data.options.method
71 | if (!data.options.body) delete data.options.body
72 |
73 | return data
74 | }
75 |
--------------------------------------------------------------------------------
/tests/setup.js:
--------------------------------------------------------------------------------
1 | import 'babel-polyfill'
2 | import FormData from 'form-data'
3 | import { expect } from 'chai'
4 |
5 | global.expect = expect
6 | global.FormData = FormData
7 |
--------------------------------------------------------------------------------
/tests/unit/body.js:
--------------------------------------------------------------------------------
1 | import processBody from '../../src/utilities/body'
2 |
3 | describe('processBody', () => {
4 | it('returns null when no value is passed', () => {
5 | const body = processBody()
6 | expect(body).to.equal(null)
7 | })
8 |
9 | it('returns null when empty string is passed', () => {
10 | const body = processBody('')
11 | expect(body).to.equal(null)
12 | })
13 |
14 | it('returns input when FormData is passed', () => {
15 | const fd = new FormData()
16 | const body = processBody(fd)
17 | expect(body).to.equal(fd)
18 | })
19 |
20 | it('returns input when stringified data is passed', () => {
21 | const data = JSON.stringify({value: 'test'})
22 | const body = processBody(data)
23 | expect(body).to.equal(data)
24 | })
25 |
26 | it('returns stringified data when object is passed', () => {
27 | const rawData = {value: 'test'}
28 | const data = JSON.stringify(rawData)
29 | const body = processBody(rawData)
30 | expect(body).to.equal(data)
31 | })
32 | })
33 |
--------------------------------------------------------------------------------
/tests/unit/fetch.js:
--------------------------------------------------------------------------------
1 | import fetch from '../../src/utilities/fetch'
2 |
3 | describe('fetch', () => {
4 | it('returns value from response', async function () {
5 | let response = await fetch('https://freegeoip.net/json/github.com')
6 |
7 | expect(response.country_code).to.equal('US')
8 | })
9 | })
10 |
--------------------------------------------------------------------------------
/tests/unit/normalize.js:
--------------------------------------------------------------------------------
1 | import normalize from '../../src/utilities/normalize'
2 |
3 | const passToNormalize = (strings, ...vars) => normalize(strings, vars)
4 |
5 | describe('normalize', () => {
6 | it('returns correct fetch options for get request', () => {
7 | let { url, options } = passToNormalize`
8 | url: http://test.app/settings/user/19
9 | method: GET
10 | `
11 |
12 | expect(url).to.equal('http://test.app/settings/user/19')
13 | expect(options.method).to.equal('GET')
14 | })
15 |
16 | it('returns url when it is a variable', () => {
17 | let { url, options } = passToNormalize`
18 | url: ${`http://test.app/${1}`}
19 | method: POST
20 | `
21 |
22 | expect(url).to.equal('http://test.app/1')
23 | expect(options.method).to.equal('POST')
24 | })
25 |
26 | it('returns body on post request', () => {
27 | let { url, options } = passToNormalize`
28 | url: http://test.app
29 | method: POST
30 | body: ${{ name: 'Bobby' }}
31 | `
32 |
33 | expect(url).to.equal('http://test.app')
34 | expect(options.method).to.equal('POST')
35 | expect(options.body).to.equal('{"name":"Bobby"}')
36 | })
37 |
38 | it('returns headers on post request', () => {
39 | let { url, options } = passToNormalize`
40 | url: http://test.app
41 | method: POST
42 | headers: ${{ Authorization: 'Bearer: Fake' }}
43 | `
44 |
45 | expect(url).to.equal('http://test.app')
46 | expect(options.method).to.equal('POST')
47 | expect(options.headers.Authorization).to.equal('Bearer: Fake')
48 | })
49 |
50 | it('returns headers and body on post request', () => {
51 | let { url, options } = passToNormalize`
52 | url: http://test.app
53 | method: POST
54 | headers: ${{ Authorization: 'Bearer: Fake' }}
55 | body: ${{ name: 'Bobby' }}
56 | `
57 |
58 | expect(url).to.equal('http://test.app')
59 | expect(options.method).to.equal('POST')
60 | expect(options.body).to.equal('{"name":"Bobby"}')
61 | expect(options.headers.Authorization).to.equal('Bearer: Fake')
62 | })
63 |
64 | it('returns headers and body on post request in reverse', () => {
65 | let { url, options } = passToNormalize`
66 | url: http://test.app
67 | method: POST
68 | body: ${{ name: 'Bobby' }}
69 | headers: ${{ Authorization: 'Bearer: Fake' }}
70 | `
71 |
72 | expect(url).to.equal('http://test.app')
73 | expect(options.method).to.equal('POST')
74 | expect(options.body).to.equal('{"name":"Bobby"}')
75 | expect(options.headers.Authorization).to.equal('Bearer: Fake')
76 | })
77 | })
78 |
--------------------------------------------------------------------------------
/tests/unit/partial.js:
--------------------------------------------------------------------------------
1 | import { partial } from '../../src'
2 |
3 | describe('partial', () => {
4 | it('returns value from response', async function () {
5 | const requests = {
6 | login: partial`
7 | url: https://freegeoip.net/json/github.com
8 | `
9 | }
10 |
11 | let response = await requests.login`
12 | method: GET
13 | `
14 | expect(response.country_code).to.equal('US')
15 | })
16 |
17 | it('returns a new function', async function () {
18 | expect(typeof partial`url: https://freegeoip.net/json/github.com`).to.equal('function')
19 | })
20 |
21 | it('overwrites partial data', async function () {
22 | const requests = {
23 | login: partial`
24 | url: https://freegeoip.net/json/github.com
25 | method: POST
26 | `
27 | }
28 |
29 | let response = await requests.login`
30 | method: GET
31 | `
32 | expect(response.country_code).to.equal('US')
33 | })
34 |
35 | it('keeps partial data if method is empty on calling', async function () {
36 | const requests = {
37 | login: partial`
38 | url: https://freegeoip.net/json/github.com
39 | method: POST
40 | `
41 | }
42 |
43 | let response = await requests.login``
44 | expect(typeof response).to.equal('object')
45 | expect(Object.keys(response).length).to.equal(0)
46 | })
47 |
48 | it('passes partial url', async function () {
49 | let test = partial`
50 | url: /test
51 | method: POST
52 | `
53 |
54 | await test`
55 | url: ${url => {
56 | expect(url).to.equal('/test')
57 | return 'https://freegeoip.net/json/github.com'
58 | }}
59 | method: GET
60 | `
61 | })
62 | })
63 |
--------------------------------------------------------------------------------
/tests/unit/request.js:
--------------------------------------------------------------------------------
1 | import request from '../../src';
2 |
3 | describe('request', () => {
4 | it('returns value from response', async function() {
5 | let response = await request`
6 | url: https://freegeoip.net/json/github.com
7 | `;
8 | expect(response.country_code).to.equal('US');
9 | });
10 |
11 | it('returns headers on response', async function() {
12 | await request`
13 | url: https://freegeoip.net/json/github.com
14 | onResponse: ${response => {
15 | expect(response.headers.get('content-type')).to.equal('application/json');
16 | }}
17 | `;
18 | });
19 | });
20 |
--------------------------------------------------------------------------------