├── .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 | [![CircleCI](https://circleci.com/gh/Legitcode/legible.svg?style=svg)](https://circleci.com/gh/Legitcode/legible) 2 | [![All Contributors](https://img.shields.io/badge/all_contributors-2-orange.svg?style=flat-square)](#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 | --------------------------------------------------------------------------------